diff --git a/x-pack/platform/plugins/shared/cases/common/api/index.ts b/x-pack/platform/plugins/shared/cases/common/api/index.ts index 7444f1e0bb7e6..6c315f929b9bb 100644 --- a/x-pack/platform/plugins/shared/cases/common/api/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/api/index.ts @@ -6,5 +6,3 @@ */ export * from './helpers'; -export * from './runtime_types'; -export * from './saved_object'; diff --git a/x-pack/platform/plugins/shared/cases/common/api/runtime_types.ts b/x-pack/platform/plugins/shared/cases/common/api/runtime_types.ts deleted file mode 100644 index 79caebbaebba7..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/api/runtime_types.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; - -import type { JsonArray, JsonObject, JsonValue } from '@kbn/utility-types'; -import { formatErrors } from '@kbn/securitysolution-io-ts-utils/src/format_errors'; -type ErrorFactory = (message: string) => Error; -export const throwErrors = (createError: ErrorFactory) => (errors: rt.Errors) => { - throw createError(formatErrors(errors).join()); -}; - -export const jsonScalarRt = rt.union([rt.null, rt.boolean, rt.number, rt.string]); - -export const jsonValueRt: rt.Type = rt.recursion('JsonValue', () => - rt.union([jsonScalarRt, jsonArrayRt, jsonObjectRt]) -); - -export const jsonArrayRt: rt.Type = rt.recursion('JsonArray', () => - rt.array(jsonValueRt) -); - -export const jsonObjectRt: rt.Type = rt.recursion('JsonObject', () => - rt.record(rt.string, jsonValueRt) -); diff --git a/x-pack/platform/plugins/shared/cases/common/api/saved_object.ts b/x-pack/platform/plugins/shared/cases/common/api/saved_object.ts deleted file mode 100644 index de2fa3fa8ef3a..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/api/saved_object.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; - -import { either } from 'fp-ts/Either'; - -export const NumberFromString = new rt.Type( - 'NumberFromString', - rt.number.is, - (u, c) => - either.chain(rt.string.validate(u, c), (s) => { - const n = +s; - return isNaN(n) ? rt.failure(u, c, 'cannot parse to a number') : rt.success(n); - }), - String -); diff --git a/x-pack/platform/plugins/shared/cases/common/files/index.test.ts b/x-pack/platform/plugins/shared/cases/common/files/index.test.ts index 0ba1741efc5f5..21d85e16b0916 100644 --- a/x-pack/platform/plugins/shared/cases/common/files/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/files/index.test.ts @@ -6,7 +6,7 @@ */ import { - CaseFileMetadataForDeletionRt, + CaseFileMetadataForDeletionSchema, constructFileKindIdByOwner, constructFilesHttpOperationPrivilege, constructOwnerFromFileKind, @@ -65,30 +65,22 @@ describe('files index', () => { }); }); - describe('CaseFileMetadataForDeletionRt', () => { + describe('CaseFileMetadataForDeletionSchema', () => { const defaultRequest = { caseIds: ['case-id-1', 'case-id-2'], }; - it('has expected attributes in request', () => { - const query = CaseFileMetadataForDeletionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('parses expected attributes in request', () => { + expect(CaseFileMetadataForDeletionSchema.parse(defaultRequest)).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CaseFileMetadataForDeletionRt.decode({ + it('strips foo:bar attributes from request', () => { + const parsed = CaseFileMetadataForDeletionSchema.parse({ caseIds: ['case-id-1', 'case-id-2'], foo: 'bar', }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(parsed).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/files/index.ts b/x-pack/platform/plugins/shared/cases/common/files/index.ts index 51f38ce46bbb2..5315f29155de0 100644 --- a/x-pack/platform/plugins/shared/cases/common/files/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/files/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { isEmpty } from 'lodash'; import { OWNERS } from '../constants'; import type { HttpApiPrivilegeOperation, Owner } from '../constants/types'; @@ -14,11 +14,11 @@ import type { HttpApiPrivilegeOperation, Owner } from '../constants/types'; * This type is only used to validate for deletion, it does not check all the fields that should exist in the file * metadata. */ -export const CaseFileMetadataForDeletionRt = rt.strict({ - caseIds: rt.array(rt.string), +export const CaseFileMetadataForDeletionSchema = z.object({ + caseIds: z.array(z.string()), }); -export type CaseFileMetadataForDeletion = rt.TypeOf; +export type CaseFileMetadataForDeletion = z.infer; const FILE_KIND_DELIMITER = 'FilesCases'; diff --git a/x-pack/platform/plugins/shared/cases/common/index.ts b/x-pack/platform/plugins/shared/cases/common/index.ts index 9b730b5c30709..7c0d68165e4a7 100644 --- a/x-pack/platform/plugins/shared/cases/common/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/index.ts @@ -71,7 +71,7 @@ export { export type { AttachmentAttributes } from './types/domain'; export { ConnectorTypes, AttachmentType, ExternalReferenceStorageType } from './types/domain'; -export { getCasesFromAlertsUrl, getCaseFindUserActionsUrl, throwErrors } from './api'; +export { getCasesFromAlertsUrl, getCaseFindUserActionsUrl } from './api'; export { createUICapabilities, type CasesUiCapabilities } from './utils/capabilities'; export { getApiTags, type CasesApiTags } from './utils/api_tags'; export { CaseMetricsFeature } from './types/api'; diff --git a/x-pack/platform/plugins/shared/cases/common/schema/index.test.ts b/x-pack/platform/plugins/shared/cases/common/schema/index.test.ts index 99fdc7b8760eb..cd44d6bc69393 100644 --- a/x-pack/platform/plugins/shared/cases/common/schema/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/schema/index.test.ts @@ -5,84 +5,78 @@ * 2.0. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; +import type { ZodType } from '@kbn/zod/v4'; import { limitedArraySchema, + limitedNumberAsIntegerSchema, limitedNumberSchema, limitedStringSchema, mimeTypeString, NonEmptyString, paginationSchema, - limitedNumberAsIntegerSchema, } from '.'; import { MAX_DOCS_PER_PAGE } from '../constants'; +const errors = (schema: ZodType, value: unknown): string[] => { + const result = schema.safeParse(value); + if (result.success) return []; + return result.error.issues.map((i) => i.message); +}; + describe('schema', () => { + describe('NonEmptyString', () => { + it('rejects an empty string', () => { + expect(errors(NonEmptyString, '')).toEqual(['string must have length >= 1']); + }); + + it('rejects whitespace-only strings (io-ts parity)', () => { + expect(errors(NonEmptyString, ' ')).toEqual(['string must have length >= 1']); + expect(errors(NonEmptyString, '\t\n')).toEqual(['string must have length >= 1']); + }); + + it('accepts a non-empty string', () => { + expect(errors(NonEmptyString, 'a')).toEqual([]); + }); + + it('preserves the original (untrimmed) value on success', () => { + const result = NonEmptyString.safeParse(' a '); + expect(result.success).toBe(true); + if (result.success) expect(result.data).toBe(' a '); + }); + }); + describe('limitedArraySchema', () => { const fieldName = 'foobar'; it('fails when given an empty string', () => { expect( - PathReporter.report( - limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }).decode(['']) - ) - ).toMatchInlineSnapshot(` - Array [ - "string must have length >= 1", - ] - `); + errors(limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }), ['']) + ).toEqual(['string must have length >= 1']); }); it('fails when given an empty array', () => { expect( - PathReporter.report( - limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }).decode([]) - ) - ).toMatchInlineSnapshot(` - Array [ - "The length of the field foobar is too short. Array must be of length >= 1.", - ] - `); + errors(limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }), []) + ).toEqual(['The length of the field foobar is too short. Array must be of length >= 1.']); }); it('fails when given an array larger than the limit of one item', () => { expect( - PathReporter.report( - limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }).decode([ - 'a', - 'b', - ]) - ) - ).toMatchInlineSnapshot(` - Array [ - "The length of the field foobar is too long. Array must be of length <= 1.", - ] - `); + errors(limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }), ['a', 'b']) + ).toEqual(['The length of the field foobar is too long. Array must be of length <= 1.']); }); it('succeeds when given an array of 1 item with a non-empty string', () => { expect( - PathReporter.report( - limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }).decode(['a']) - ) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + errors(limitedArraySchema({ codec: NonEmptyString, fieldName, min: 1, max: 1 }), ['a']) + ).toEqual([]); }); - it('succeeds when given an array of 0 item with a non-empty string when the min is 0', () => { + it('succeeds when given an array of 0 items when the min is 0', () => { expect( - PathReporter.report( - limitedArraySchema({ codec: NonEmptyString, fieldName, min: 0, max: 2 }).decode([]) - ) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + errors(limitedArraySchema({ codec: NonEmptyString, fieldName, min: 0, max: 2 }), []) + ).toEqual([]); }); }); @@ -90,300 +84,168 @@ describe('schema', () => { const fieldName = 'foo'; it('fails when given string is shorter than minimum', () => { - expect(PathReporter.report(limitedStringSchema({ fieldName, min: 2, max: 1 }).decode('a'))) - .toMatchInlineSnapshot(` - Array [ - "The length of the foo is too short. The minimum length is 2.", - ] - `); + expect(errors(limitedStringSchema({ fieldName, min: 2, max: 5 }), 'a')).toEqual([ + 'The length of the foo is too short. The minimum length is 2.', + ]); }); it('fails when given string is empty and minimum is not 0', () => { - expect(PathReporter.report(limitedStringSchema({ fieldName, min: 1, max: 1 }).decode(''))) - .toMatchInlineSnapshot(` - Array [ - "The foo field cannot be an empty string.", - ] - `); + expect(errors(limitedStringSchema({ fieldName, min: 1, max: 1 }), '')).toEqual([ + 'The foo field cannot be an empty string.', + ]); }); - it('fails when given string consists only empty characters and minimum is not 0', () => { - expect(PathReporter.report(limitedStringSchema({ fieldName, min: 1, max: 1 }).decode(' '))) - .toMatchInlineSnapshot(` - Array [ - "The foo field cannot be an empty string.", - ] - `); + it('fails when given string consists only of whitespace and minimum is not 0', () => { + expect(errors(limitedStringSchema({ fieldName, min: 1, max: 1 }), ' ')).toEqual([ + 'The foo field cannot be an empty string.', + ]); }); it('fails when given string is larger than maximum', () => { - expect( - PathReporter.report( - limitedStringSchema({ fieldName, min: 1, max: 5 }).decode('Hello there!!') - ) - ).toMatchInlineSnapshot(` - Array [ - "The length of the foo is too long. The maximum length is 5.", - ] - `); + expect(errors(limitedStringSchema({ fieldName, min: 1, max: 5 }), 'Hello there!!')).toEqual([ + 'The length of the foo is too long. The maximum length is 5.', + ]); }); it('succeeds when given string within limit', () => { - expect( - PathReporter.report(limitedStringSchema({ fieldName, min: 1, max: 50 }).decode('Hello!!')) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + expect(errors(limitedStringSchema({ fieldName, min: 1, max: 50 }), 'Hello!!')).toEqual([]); }); it('succeeds when given string is empty and minimum is 0', () => { - expect(PathReporter.report(limitedStringSchema({ fieldName, min: 0, max: 5 }).decode(''))) - .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + expect(errors(limitedStringSchema({ fieldName, min: 0, max: 5 }), '')).toEqual([]); }); - it('succeeds when given string consists only empty characters and minimum is 0', () => { - expect(PathReporter.report(limitedStringSchema({ fieldName, min: 0, max: 5 }).decode(' '))) - .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + it('succeeds when given string consists only of whitespace and minimum is 0', () => { + expect(errors(limitedStringSchema({ fieldName, min: 0, max: 5 }), ' ')).toEqual([]); }); - it('succeeds when given string is same as maximum', () => { - expect( - PathReporter.report(limitedStringSchema({ fieldName, min: 0, max: 5 }).decode('Hello')) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + it('succeeds when given string is same length as maximum', () => { + expect(errors(limitedStringSchema({ fieldName, min: 0, max: 5 }), 'Hello')).toEqual([]); }); - it('succeeds when given string is larger than maximum but same as maximum after trim', () => { - expect( - PathReporter.report(limitedStringSchema({ fieldName, min: 0, max: 5 }).decode('Hello ')) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + it('succeeds when trimmed length is within maximum (trailing whitespace ignored)', () => { + expect(errors(limitedStringSchema({ fieldName, min: 0, max: 5 }), 'Hello ')).toEqual([]); }); }); describe('paginationSchema', () => { it('succeeds when no page or perPage passed', () => { - expect(PathReporter.report(paginationSchema({ maxPerPage: 1 }).decode({}))) - .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + expect(errors(paginationSchema({ maxPerPage: 1 }), {})).toEqual([]); }); it('succeeds when only valid page is passed', () => { - expect(PathReporter.report(paginationSchema({ maxPerPage: 2 }).decode({ page: 0 }))) - .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + expect(errors(paginationSchema({ maxPerPage: 2 }), { page: 0 })).toEqual([]); }); it('succeeds when only valid perPage is passed', () => { - expect(PathReporter.report(paginationSchema({ maxPerPage: 3 }).decode({ perPage: 1 }))) - .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + expect(errors(paginationSchema({ maxPerPage: 3 }), { perPage: 1 })).toEqual([]); }); it('succeeds when page and perPage are passed and valid', () => { - expect( - PathReporter.report(paginationSchema({ maxPerPage: 3 }).decode({ page: 1, perPage: 2 })) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + expect(errors(paginationSchema({ maxPerPage: 3 }), { page: 1, perPage: 2 })).toEqual([]); }); it('fails when perPage > maxPerPage', () => { - expect(PathReporter.report(paginationSchema({ maxPerPage: 3 }).decode({ perPage: 4 }))) - .toMatchInlineSnapshot(` - Array [ - "The provided perPage value is too high. The maximum allowed perPage value is 3.", - ] - `); + expect(errors(paginationSchema({ maxPerPage: 3 }), { perPage: 4 })).toEqual([ + 'The provided perPage value is too high. The maximum allowed perPage value is 3.', + ]); }); it(`fails when page > ${MAX_DOCS_PER_PAGE}`, () => { - expect( - PathReporter.report( - paginationSchema({ maxPerPage: 3 }).decode({ page: MAX_DOCS_PER_PAGE + 1 }) - ) - ).toMatchInlineSnapshot(` - Array [ - "The number of documents is too high. Paginating through more than 10000 documents is not possible.", - ] - `); + expect(errors(paginationSchema({ maxPerPage: 3 }), { page: MAX_DOCS_PER_PAGE + 1 })).toEqual([ + 'The number of documents is too high. Paginating through more than 10000 documents is not possible.', + ]); }); it(`fails when page * perPage > ${MAX_DOCS_PER_PAGE}`, () => { expect( - PathReporter.report( - paginationSchema({ maxPerPage: 3 }).decode({ page: MAX_DOCS_PER_PAGE, perPage: 2 }) - ) - ).toMatchInlineSnapshot(` - Array [ - "The number of documents is too high. Paginating through more than 10000 documents is not possible.", - ] - `); + errors(paginationSchema({ maxPerPage: 3 }), { page: MAX_DOCS_PER_PAGE, perPage: 2 }) + ).toEqual([ + 'The number of documents is too high. Paginating through more than 10000 documents is not possible.', + ]); }); - it('validate params as strings work correctly', () => { - expect( - PathReporter.report(paginationSchema({ maxPerPage: 3 }).decode({ page: '1', perPage: '2' })) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + it('accepts numeric strings for page/perPage', () => { + expect(errors(paginationSchema({ maxPerPage: 3 }), { page: '1', perPage: '2' })).toEqual([]); }); - it('invalid NumberFromString work correctly', () => { - expect( - PathReporter.report(paginationSchema({ maxPerPage: 3 }).decode({ page: 'a', perPage: 'b' })) - ).toMatchInlineSnapshot(` - Array [ - "Invalid value \\"a\\" supplied to : Pagination/page: (number | NumberFromString)/0: number", - "cannot parse to a number", - "Invalid value \\"b\\" supplied to : Pagination/perPage: (number | NumberFromString)/0: number", - "cannot parse to a number", - ] - `); + it('rejects non-numeric strings for page/perPage (NumberFromString parity)', () => { + const result = paginationSchema({ maxPerPage: 3 }).safeParse({ page: 'a', perPage: 'b' }); + expect(result.success).toBe(false); + if (!result.success) { + const messages = result.error.issues.map((i) => i.message); + expect(messages).toEqual( + expect.arrayContaining(['cannot parse to a number', 'cannot parse to a number']) + ); + } }); }); describe('limitedNumberSchema', () => { - it('works correctly the number is between min and max', () => { - expect( - PathReporter.report(limitedNumberSchema({ fieldName: 'foo', min: 0, max: 2 }).decode(1)) - ).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + it('succeeds when the number is within range', () => { + expect(errors(limitedNumberSchema({ fieldName: 'foo', min: 0, max: 2 }), 1)).toEqual([]); }); - it('fails when given a number that is lower than the minimum', () => { - expect( - PathReporter.report(limitedNumberSchema({ fieldName: 'foo', min: 1, max: 2 }).decode(0)) - ).toMatchInlineSnapshot(` - Array [ - "The foo field cannot be less than 1.", - ] - `); + it('fails when given a number lower than the minimum', () => { + expect(errors(limitedNumberSchema({ fieldName: 'foo', min: 1, max: 2 }), 0)).toEqual([ + 'The foo field cannot be less than 1.', + ]); }); - it('fails when given number that is higher than the maximum', () => { - expect( - PathReporter.report(limitedNumberSchema({ fieldName: 'foo', min: 1, max: 2 }).decode(3)) - ).toMatchInlineSnapshot(` - Array [ - "The foo field cannot be more than 2.", - ] - `); + it('fails when given a number higher than the maximum', () => { + expect(errors(limitedNumberSchema({ fieldName: 'foo', min: 1, max: 2 }), 3)).toEqual([ + 'The foo field cannot be more than 2.', + ]); }); }); describe('mimeTypeString', () => { - it('works correctly when the value is an allowed mime type', () => { - expect(PathReporter.report(mimeTypeString.decode('image/jpx'))).toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + it('succeeds when the value is an allowed mime type', () => { + expect(errors(mimeTypeString, 'image/jpx')).toEqual([]); }); it('fails when the value is not an allowed mime type', () => { - expect(PathReporter.report(mimeTypeString.decode('foo/bar'))).toMatchInlineSnapshot(` - Array [ - "The mime type field value foo/bar is not allowed.", - ] - `); + expect(errors(mimeTypeString, 'foo/bar')).toEqual([ + 'The mime type field value foo/bar is not allowed.', + ]); }); }); describe('limitedNumberAsIntegerSchema', () => { - it('works correctly the number is safe integer', () => { - expect(PathReporter.report(limitedNumberAsIntegerSchema({ fieldName: 'foo' }).decode(1))) - .toMatchInlineSnapshot(` - Array [ - "No errors!", - ] - `); + it('succeeds when the number is a safe integer', () => { + expect(errors(limitedNumberAsIntegerSchema({ fieldName: 'foo' }), 1)).toEqual([]); }); - it('fails when given a number that is lower than the minimum', () => { + it('fails when given a number lower than MIN_SAFE_INTEGER', () => { 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.", - ] - `); + errors(limitedNumberAsIntegerSchema({ fieldName: 'foo' }), Number.MIN_SAFE_INTEGER - 1) + ).toEqual([ + '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', () => { + it('fails when given a number higher than MAX_SAFE_INTEGER', () => { 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.", - ] - `); + errors(limitedNumberAsIntegerSchema({ fieldName: 'foo' }), Number.MAX_SAFE_INTEGER + 1) + ).toEqual([ + 'The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.', + ]); + }); + + it('fails when given null', () => { + const result = limitedNumberAsIntegerSchema({ fieldName: 'foo' }).safeParse(null); + expect(result.success).toBe(false); + }); + + it('fails when given a string', () => { + const result = limitedNumberAsIntegerSchema({ fieldName: 'foo' }).safeParse('some string'); + expect(result.success).toBe(false); + }); + + it('fails when given a float', () => { + expect(errors(limitedNumberAsIntegerSchema({ fieldName: 'foo' }), 1.2)).toEqual([ + 'The foo field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.', + ]); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/schema/index.ts b/x-pack/platform/plugins/shared/cases/common/schema/index.ts index 72cba578e5ee0..44ac754c5dc3d 100644 --- a/x-pack/platform/plugins/shared/cases/common/schema/index.ts +++ b/x-pack/platform/plugins/shared/cases/common/schema/index.ts @@ -5,13 +5,9 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { either } from 'fp-ts/Either'; - -import { MAX_DOCS_PER_PAGE } from '../constants'; -import type { PartialPaginationType } from './types'; -import { PaginationSchemaRt } from './types'; +import { z } from '@kbn/zod/v4'; import { ALLOWED_MIME_TYPES } from '../constants/mime_types'; +import { MAX_DOCS_PER_PAGE } from '../constants'; export interface LimitedSchemaType { fieldName: string; @@ -19,193 +15,180 @@ export interface LimitedSchemaType { max: number; } -export const NonEmptyString = new rt.Type( - 'NonEmptyString', - rt.string.is, - (input, context) => - either.chain(rt.string.validate(input, context), (s) => { - if (s.trim() !== '') { - return rt.success(s); - } else { - return rt.failure(input, context, 'string must have length >= 1'); - } - }), - rt.identity -); +// Matches io-ts parity: rejects strings whose `.trim()` is empty (e.g. " ", "\t\n"). +// Preserves the original (untrimmed) string on success. +export const NonEmptyString = z + .string() + .refine((s) => s.trim().length >= 1, 'string must have length >= 1'); export const limitedStringSchema = ({ fieldName, min, max }: LimitedSchemaType) => - new rt.Type( - 'LimitedString', - rt.string.is, - (input, context) => - either.chain(rt.string.validate(input, context), (s) => { - const trimmedString = s.trim(); - - if (trimmedString.length === 0 && trimmedString.length < min) { - return rt.failure(input, context, `The ${fieldName} field cannot be an empty string.`); - } - - if (trimmedString.length < min) { - return rt.failure( - input, - context, - `The length of the ${fieldName} is too short. The minimum length is ${min}.` - ); - } - - if (trimmedString.length > max) { - return rt.failure( - input, - context, - `The length of the ${fieldName} is too long. The maximum length is ${max}.` - ); - } - - return rt.success(s); - }), - rt.identity - ); - -export const limitedArraySchema = ({ + z.string().superRefine((s, ctx) => { + const trimmed = s.trim(); + + // io-ts parity: an empty / whitespace-only string is only rejected when + // `min > 0`; with `min === 0` it should pass through. + if (trimmed.length === 0 && min > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The ${fieldName} field cannot be an empty string.`, + }); + return; + } + + if (trimmed.length < min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The length of the ${fieldName} is too short. The minimum length is ${min}.`, + }); + return; + } + + if (trimmed.length > max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The length of the ${fieldName} is too long. The maximum length is ${max}.`, + }); + } + }); + +export const limitedArraySchema = ({ codec, fieldName, min, max, }: { codec: T } & LimitedSchemaType) => - new rt.Type>, Array>, unknown>( - 'LimitedArray', - (input): input is T[] => rt.array(codec).is(input), - (input, context) => - either.chain(rt.array(codec).validate(input, context), (s) => { - if (s.length < min) { - return rt.failure( - input, - context, - `The length of the field ${fieldName} is too short. Array must be of length >= ${min}.` - ); - } - - if (s.length > max) { - return rt.failure( - input, - context, - `The length of the field ${fieldName} is too long. Array must be of length <= ${max}.` - ); - } - - return rt.success(s); - }), - rt.identity - ); - -export const paginationSchema = ({ maxPerPage }: { maxPerPage: number }) => - new rt.PartialType( - 'Pagination', - PaginationSchemaRt.is, - (u, c) => - either.chain(PaginationSchemaRt.validate(u, c), (params) => { - if (params.page == null && params.perPage == null) { - return rt.success(params); - } - - const pageAsNumber = params.page ?? 0; - const perPageAsNumber = params.perPage ?? 0; - - if (perPageAsNumber > maxPerPage) { - return rt.failure( - u, - c, - `The provided perPage value is too high. The maximum allowed perPage value is ${maxPerPage}.` - ); - } - - if (Math.max(pageAsNumber, pageAsNumber * perPageAsNumber) > MAX_DOCS_PER_PAGE) { - return rt.failure( - u, - c, - `The number of documents is too high. Paginating through more than ${MAX_DOCS_PER_PAGE} documents is not possible.` - ); - } - - return rt.success({ - ...(params.page != null && { page: pageAsNumber }), - ...(params.perPage != null && { perPage: perPageAsNumber }), - }); - }), - rt.identity, - undefined - ); + z.array(codec).superRefine((s, ctx) => { + if (s.length < min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The length of the field ${fieldName} is too short. Array must be of length >= ${min}.`, + }); + return; + } + + if (s.length > max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The length of the field ${fieldName} is too long. Array must be of length <= ${max}.`, + }); + } + }); export const limitedNumberSchema = ({ fieldName, min, max }: LimitedSchemaType) => - new rt.Type( - 'LimitedNumber', - rt.number.is, - (input, context) => - either.chain(rt.number.validate(input, context), (s) => { - if (s < min) { - return rt.failure(input, context, `The ${fieldName} field cannot be less than ${min}.`); - } - - if (s > max) { - return rt.failure(input, context, `The ${fieldName} field cannot be more than ${max}.`); - } - - return rt.success(s); - }), - rt.identity - ); + z.number().superRefine((s, ctx) => { + if (s < min) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The ${fieldName} field cannot be less than ${min}.`, + }); + return; + } + + if (s > max) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The ${fieldName} field cannot be more than ${max}.`, + }); + } + }); + +export const paginationSchema = ({ maxPerPage }: { maxPerPage: number }) => { + // Matches io-ts `NumberFromString` parity: a string input must parse to a finite + // number. `Number('abc')` returns `NaN`, which previously failed validation — + // a plain `transform((s) => Number(s))` would let `NaN` through silently and + // bypass the downstream `> maxPerPage` / `MAX_DOCS_PER_PAGE` guards. + // The transform is on the union (rather than only on the string variant) so + // the custom error message survives instead of being swallowed by the + // union's "no variant matched" error. + const pageCoerce = z.union([z.number(), z.string()]).transform((value, ctx) => { + if (typeof value === 'number') return value; + const n = Number(value); + if (!Number.isFinite(n)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'cannot parse to a number' }); + return z.NEVER; + } + return n; + }); + return z + .object({ + page: pageCoerce.optional(), + perPage: pageCoerce.optional(), + }) + .superRefine((params, ctx) => { + if (params.page == null && params.perPage == null) return; + + const pageAsNumber = params.page ?? 0; + const perPageAsNumber = params.perPage ?? 0; + + if (perPageAsNumber > maxPerPage) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The provided perPage value is too high. The maximum allowed perPage value is ${maxPerPage}.`, + }); + return; + } + + if (Math.max(pageAsNumber, pageAsNumber * perPageAsNumber) > MAX_DOCS_PER_PAGE) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The number of documents is too high. Paginating through more than ${MAX_DOCS_PER_PAGE} documents is not possible.`, + }); + } + }); +}; export const limitedNumberAsIntegerSchema = ({ fieldName }: { fieldName: string }) => - new rt.Type( - '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; + z.number().superRefine((s, ctx) => { + if (!Number.isSafeInteger(s)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The ${fieldName} field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`, + }); + } + }); + +export const regexStringSchema = ({ + codec, + pattern, + message, +}: { + codec: z.ZodType; pattern: string; message: string; -} - -export const regexStringRt = ({ codec, pattern, message }: RegexStringSchemaType) => - new rt.Type( - 'RegexString', - codec.is, - (input, context) => - either.chain(codec.validate(input, context), (value) => { - const regex = new RegExp(pattern, 'g'); - - if (!regex.test(value)) { - return rt.failure(input, context, message); - } - - return rt.success(value); - }), - rt.identity - ); - -export const mimeTypeString = new rt.Type( - 'mimeTypeString', - rt.string.is, - (input, context) => - either.chain(rt.string.validate(input, context), (s) => { - if (!ALLOWED_MIME_TYPES.includes(s)) { - return rt.failure(input, context, `The mime type field value ${s} is not allowed.`); - } - - return rt.success(s); - }), - rt.identity +}) => + codec.superRefine((value, ctx) => { + if (!new RegExp(pattern).test(value)) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message }); + } + }); + +export const mimeTypeString = z.string().superRefine((s, ctx) => { + if (!ALLOWED_MIME_TYPES.includes(s)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `The mime type field value ${s} is not allowed.`, + }); + } +}); + +/** + * Zod equivalent of jsonValueRt — a recursive JSON value type. + */ +export type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + +export const jsonValueSchema: z.ZodType = z.lazy(() => + z.union([ + z.string(), + z.number(), + z.boolean(), + z.null(), + z.array(jsonValueSchema), + z.record(z.string(), jsonValueSchema), + ]) ); diff --git a/x-pack/platform/plugins/shared/cases/common/schema/types.ts b/x-pack/platform/plugins/shared/cases/common/schema/types.ts deleted file mode 100644 index d2ece26504254..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/schema/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as rt from 'io-ts'; -import { NumberFromString } from '../api/saved_object'; - -const PageTypeRt = rt.union([rt.number, NumberFromString]); -type PageNumberType = rt.TypeOf; - -export interface Pagination { - page: PageNumberType; - perPage: PageNumberType; -} - -export const PaginationSchemaRt = rt.exact(rt.partial({ page: PageTypeRt, perPage: PageTypeRt })); -export type PartialPaginationType = Partial; diff --git a/x-pack/platform/plugins/shared/cases/common/schema_zod/index.ts b/x-pack/platform/plugins/shared/cases/common/schema_zod/index.ts deleted file mode 100644 index bc0289184465e..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/schema_zod/index.ts +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { ALLOWED_MIME_TYPES } from '../constants/mime_types'; - -export interface LimitedSchemaType { - fieldName: string; - min: number; - max: number; -} - -export const NonEmptyString = z.string().min(1); - -export const limitedStringSchema = ({ fieldName, min, max }: LimitedSchemaType) => - z.string().superRefine((s, ctx) => { - const trimmed = s.trim(); - - if (trimmed.length === 0) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The ${fieldName} field cannot be an empty string.`, - }); - return; - } - - if (trimmed.length < min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The length of the ${fieldName} is too short. The minimum length is ${min}.`, - }); - return; - } - - if (trimmed.length > max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The length of the ${fieldName} is too long. The maximum length is ${max}.`, - }); - } - }); - -export const limitedArraySchema = ({ - codec, - fieldName, - min, - max, -}: { codec: T } & LimitedSchemaType) => - z.array(codec).superRefine((s, ctx) => { - if (s.length < min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The length of the field ${fieldName} is too short. Array must be of length >= ${min}.`, - }); - return; - } - - if (s.length > max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The length of the field ${fieldName} is too long. Array must be of length <= ${max}.`, - }); - } - }); - -export const limitedNumberSchema = ({ fieldName, min, max }: LimitedSchemaType) => - z.number().superRefine((s, ctx) => { - if (s < min) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The ${fieldName} field cannot be less than ${min}.`, - }); - return; - } - - if (s > max) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The ${fieldName} field cannot be more than ${max}.`, - }); - } - }); - -export const paginationSchema = ({ maxPerPage }: { maxPerPage: number }) => { - const pageCoerce = z.union([z.number(), z.string().transform((s) => Number(s))]); - return z.object({ - page: pageCoerce.optional(), - perPage: pageCoerce.optional(), - }); -}; - -export const limitedNumberAsIntegerSchema = ({ fieldName }: { fieldName: string }) => - z.number().superRefine((s, ctx) => { - if (!Number.isSafeInteger(s)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The ${fieldName} field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.`, - }); - } - }); - -export const regexStringSchema = ({ - codec, - pattern, - message, -}: { - codec: z.ZodType; - pattern: string; - message: string; -}) => - codec.superRefine((value, ctx) => { - if (!new RegExp(pattern).test(value)) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message }); - } - }); - -export const mimeTypeString = z.string().superRefine((s, ctx) => { - if (!ALLOWED_MIME_TYPES.includes(s)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `The mime type field value ${s} is not allowed.`, - }); - } -}); - -/** - * Zod equivalent of jsonValueRt — a recursive JSON value type. - */ -export type JsonValue = - | string - | number - | boolean - | null - | JsonValue[] - | { [key: string]: JsonValue }; - -export const jsonValueSchema: z.ZodType = z.lazy(() => - z.union([ - z.string(), - z.number(), - z.boolean(), - z.null(), - z.array(jsonValueSchema), - z.record(z.string(), jsonValueSchema), - ]) -); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.test.ts index c9817bd3adc6e..de7fcd16da9a5 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.test.ts @@ -5,565 +5,344 @@ * 2.0. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; import { MAX_BULK_CREATE_ATTACHMENTS, + MAX_BULK_GET_ATTACHMENTS, MAX_COMMENT_LENGTH, - MAX_FILENAME_LENGTH, + MAX_DELETE_FILES, } from '../../../constants'; -import { AttachmentType } from '../../domain/attachment/v1'; -import { - AttachmentPatchRequestRt, - AttachmentRequestRt, - AttachmentsFindResponseRt, - BulkCreateAttachmentsRequestRt, - BulkDeleteFileAttachmentsRequestRt, - BulkGetAttachmentsRequestRt, - BulkGetAttachmentsResponseRt, - FindAttachmentsQueryParamsRt, - PostFileAttachmentRequestRt, -} from './v1'; +import type { ZodType } from '@kbn/zod/v4'; +import { DeepStrict } from '@kbn/zod-helpers'; +import { AttachmentType, ExternalReferenceStorageType } from '../../domain/attachment/v1'; import { AttachmentPatchRequestSchema, AttachmentRequestSchema, - AttachmentsFindResponseSchema, BulkCreateAttachmentsRequestSchema, BulkDeleteFileAttachmentsRequestSchema, BulkGetAttachmentsRequestSchema, - BulkGetAttachmentsResponseSchema, - FindAttachmentsQueryParamsSchema, - PostFileAttachmentRequestSchema, -} from '../../api_zod/attachment/v1'; - -describe('Attachments', () => { - describe('BulkDeleteFileAttachmentsRequestRt', () => { - it('has expected attributes in request', () => { - const query = BulkDeleteFileAttachmentsRequestRt.decode({ ids: ['abc', 'xyz'] }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ids: ['abc', 'xyz'] }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = BulkDeleteFileAttachmentsRequestRt.decode({ - ids: ['abc', 'xyz'], - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ids: ['abc', 'xyz'] }, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = BulkDeleteFileAttachmentsRequestSchema.safeParse({ ids: ['abc', 'xyz'] }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ids: ['abc', 'xyz'] }); - }); +} from './v1'; - it('zod: strips unknown fields', () => { - const result = BulkDeleteFileAttachmentsRequestSchema.safeParse({ - ids: ['abc', 'xyz'], - foo: 'bar', - }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ids: ['abc', 'xyz'] }); +const errors = (schema: ZodType, value: unknown): string[] => { + const result = schema.safeParse(value); + if (result.success) return []; + return result.error.issues.map((i) => i.message); +}; + +const validUserComment = { + type: AttachmentType.user, + comment: 'Hello', + owner: 'cases', +}; + +const validAlert = { + type: AttachmentType.alert, + alertId: ['alert-1'], + index: ['.alerts-1'], + rule: { id: 'rule-1', name: 'Rule One' }, + owner: 'cases', +}; + +const validEvent = { + type: AttachmentType.event, + eventId: ['evt-1'], + index: ['.events-1'], + owner: 'cases', +}; + +const validActions = { + type: AttachmentType.actions, + comment: 'Isolating host', + actions: { + targets: [{ hostname: 'host-1', endpointId: 'ep-1' }], + type: 'isolate', + }, + owner: 'cases', +}; + +const validExternalRefNoSO = { + type: AttachmentType.externalReference, + externalReferenceId: 'ext-1', + externalReferenceStorage: { type: ExternalReferenceStorageType.elasticSearchDoc }, + externalReferenceAttachmentTypeId: 'foo', + externalReferenceMetadata: null, + owner: 'cases', +}; + +const validExternalRefSO = { + type: AttachmentType.externalReference, + externalReferenceId: 'ext-2', + externalReferenceStorage: { + type: ExternalReferenceStorageType.savedObject, + soType: 'my-so-type', + }, + externalReferenceAttachmentTypeId: 'foo', + externalReferenceMetadata: { foo: 'bar' }, + owner: 'cases', +}; + +const validPersistableState = { + type: AttachmentType.persistableState, + persistableStateAttachmentTypeId: 'persistable-1', + persistableStateAttachmentState: { key: 'value' }, + owner: 'cases', +}; + +describe('AttachmentRequestSchema', () => { + describe('user comment variant', () => { + it('accepts a valid user comment', () => { + expect(AttachmentRequestSchema.safeParse(validUserComment).success).toBe(true); + }); + + it('rejects an empty comment string', () => { + expect(errors(AttachmentRequestSchema, { ...validUserComment, comment: '' })).toContain( + 'The comment field cannot be an empty string.' + ); + }); + + it('rejects whitespace-only comment (limitedStringSchema parity)', () => { + expect(errors(AttachmentRequestSchema, { ...validUserComment, comment: ' ' })).toContain( + 'The comment field cannot be an empty string.' + ); + }); + + it(`rejects a comment longer than MAX_COMMENT_LENGTH (${MAX_COMMENT_LENGTH})`, () => { + const comment = 'a'.repeat(MAX_COMMENT_LENGTH + 1); + expect(errors(AttachmentRequestSchema, { ...validUserComment, comment })).toContain( + `The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + ); + }); + + it('rejects a missing owner', () => { + const { owner, ...rest } = validUserComment; + expect(AttachmentRequestSchema.safeParse(rest).success).toBe(false); }); }); - describe('AttachmentRequestRt', () => { - const defaultRequest = { - comment: 'Solve this fast!', - type: AttachmentType.user, - owner: 'cases', - }; - - it('has expected attributes in request', () => { - const query = AttachmentRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + describe('alert variant', () => { + it('accepts a valid alert payload (array form)', () => { + expect(AttachmentRequestSchema.safeParse(validAlert).success).toBe(true); }); - it('removes foo:bar attributes from request', () => { - const query = AttachmentRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('accepts a valid alert payload (string form for alertId / index)', () => { + const result = AttachmentRequestSchema.safeParse({ + ...validAlert, + alertId: 'alert-1', + index: '.alerts-1', }); - }); - - it('zod: has expected attributes in request', () => { - const result = AttachmentRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { - const result = AttachmentRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + it('accepts null rule id and name (legacy alerts)', () => { + const result = AttachmentRequestSchema.safeParse({ + ...validAlert, + rule: { id: null, name: null }, + }); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); }); - describe('errors', () => { - describe('commentType: user', () => { - it('throws error when comment is too long', () => { - const longComment = 'x'.repeat(MAX_COMMENT_LENGTH + 1); - - expect( - PathReporter.report( - AttachmentRequestRt.decode({ ...defaultRequest, comment: longComment }) - ) - ).toContain('The length of the comment is too long. The maximum length is 30000.'); - }); - - it('throws error when comment is empty', () => { - expect( - PathReporter.report(AttachmentRequestRt.decode({ ...defaultRequest, comment: '' })) - ).toContain('The comment field cannot be an empty string.'); - }); - - it('throws error when comment string of empty characters', () => { - expect( - PathReporter.report(AttachmentRequestRt.decode({ ...defaultRequest, comment: ' ' })) - ).toContain('The comment field cannot be an empty string.'); - }); - }); - - describe('commentType: action', () => { - const request = { - type: AttachmentType.actions, - actions: { - targets: [ - { - hostname: 'host1', - endpointId: '001', - }, - ], - type: 'isolate', - }, - owner: 'cases', - }; - - it('throws error when comment is too long', () => { - const longComment = 'x'.repeat(MAX_COMMENT_LENGTH + 1); - - expect( - PathReporter.report(AttachmentRequestRt.decode({ ...request, comment: longComment })) - ).toContain('The length of the comment is too long. The maximum length is 30000.'); - }); - - it('throws error when comment is empty', () => { - expect( - PathReporter.report(AttachmentRequestRt.decode({ ...request, comment: '' })) - ).toContain('The comment field cannot be an empty string.'); - }); - - it('throws error when comment string of empty characters', () => { - expect( - PathReporter.report(AttachmentRequestRt.decode({ ...request, comment: ' ' })) - ).toContain('The comment field cannot be an empty string.'); - }); - }); + it('rejects a missing rule', () => { + const { rule, ...rest } = validAlert; + expect(AttachmentRequestSchema.safeParse(rest).success).toBe(false); }); }); - describe('AttachmentPatchRequestRt', () => { - const defaultRequest = { - alertId: 'alert-id-1', - index: 'alert-index-1', - type: AttachmentType.alert, - id: 'alert-comment-id', - owner: 'cases', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - }, - version: 'WzQ3LDFc', - }; - it('has expected attributes in request', () => { - const query = AttachmentPatchRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + describe('event variant', () => { + it('accepts a valid event payload', () => { + expect(AttachmentRequestSchema.safeParse(validEvent).success).toBe(true); }); - it('removes foo:bar attributes from request', () => { - const query = AttachmentPatchRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = AttachmentPatchRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = AttachmentPatchRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('rejects a missing eventId', () => { + const { eventId, ...rest } = validEvent; + expect(AttachmentRequestSchema.safeParse(rest).success).toBe(false); }); }); - describe('AttachmentsFindResponseRt', () => { - const defaultRequest = { - comments: [ - { - comment: 'Solve this fast!', - type: AttachmentType.user, - owner: 'cases', - created_at: '2020-02-19T23:06:33.798Z', - created_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, - id: 'basic-comment-id', - version: 'WzQ3LDFc', - }, - ], - page: 1, - per_page: 10, - total: 1, - }; - it('has expected attributes in request', () => { - const query = AttachmentsFindResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + describe('actions variant', () => { + it('accepts a valid actions payload', () => { + expect(AttachmentRequestSchema.safeParse(validActions).success).toBe(true); }); - it('removes foo:bar attributes from request', () => { - const query = AttachmentsFindResponseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('rejects an empty actions comment', () => { + expect(errors(AttachmentRequestSchema, { ...validActions, comment: '' })).toContain( + 'The comment field cannot be an empty string.' + ); }); - it('removes foo:bar attributes from comments', () => { - const query = AttachmentsFindResponseRt.decode({ - ...defaultRequest, - comments: [{ ...defaultRequest.comments[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('rejects malformed actions.targets', () => { + const result = AttachmentRequestSchema.safeParse({ + ...validActions, + actions: { ...validActions.actions, targets: [{ hostname: 'h' }] }, }); - }); - - it('zod: has expected attributes in request', () => { - const result = AttachmentsFindResponseSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = AttachmentsFindResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + expect(result.success).toBe(false); }); }); - describe('FindAttachmentsQueryParamsRt', () => { - const defaultRequest = { - page: 1, - perPage: 10, - sortOrder: 'asc', - }; - - it('has expected attributes in request', () => { - const query = FindAttachmentsQueryParamsRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + describe('externalReference variant', () => { + it('accepts the elasticSearchDoc-storage form', () => { + expect(AttachmentRequestSchema.safeParse(validExternalRefNoSO).success).toBe(true); }); - it('removes foo:bar attributes from request', () => { - const query = FindAttachmentsQueryParamsRt.decode({ ...defaultRequest, foo: 'bar' }); + it('accepts the savedObject-storage form', () => { + expect(AttachmentRequestSchema.safeParse(validExternalRefSO).success).toBe(true); + }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('rejects an unknown storage type', () => { + const result = AttachmentRequestSchema.safeParse({ + ...validExternalRefNoSO, + externalReferenceStorage: { type: 'mongo' }, }); + expect(result.success).toBe(false); }); + }); - it('zod: has expected attributes in request', () => { - const result = FindAttachmentsQueryParamsSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + describe('persistableState variant', () => { + it('accepts a valid persistableState payload', () => { + expect(AttachmentRequestSchema.safeParse(validPersistableState).success).toBe(true); }); - it('zod: strips unknown fields', () => { - const result = FindAttachmentsQueryParamsSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('rejects a missing persistableStateAttachmentState', () => { + const { persistableStateAttachmentState, ...rest } = validPersistableState; + expect(AttachmentRequestSchema.safeParse(rest).success).toBe(false); }); }); - describe('BulkCreateAttachmentsRequestRt', () => { - const defaultRequest = [ - { - comment: 'Solve this fast!', - type: AttachmentType.user, + describe('union dispatch', () => { + it('rejects an unknown type literal', () => { + const result = AttachmentRequestSchema.safeParse({ + type: 'banana', owner: 'cases', - }, - ]; - - it('has expected attributes in request', () => { - const query = BulkCreateAttachmentsRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = BulkCreateAttachmentsRequestRt.decode([ - { comment: 'Solve this fast!', type: AttachmentType.user, owner: 'cases', foo: 'bar' }, - ]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = BulkCreateAttachmentsRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = BulkCreateAttachmentsRequestSchema.safeParse([ - { comment: 'Solve this fast!', type: AttachmentType.user, owner: 'cases', foo: 'bar' }, - ]); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - describe('errors', () => { - it(`throws error when attachments are more than ${MAX_BULK_CREATE_ATTACHMENTS}`, () => { - const comment = { - comment: 'Solve this fast!', - type: AttachmentType.user, - owner: 'cases', - }; - const attachments = Array(MAX_BULK_CREATE_ATTACHMENTS + 1).fill(comment); - - expect(PathReporter.report(BulkCreateAttachmentsRequestRt.decode(attachments))).toContain( - `The length of the field attachments is too long. Array must be of length <= ${MAX_BULK_CREATE_ATTACHMENTS}.` - ); - }); - - it(`no errors when empty array of attachments`, () => { - expect(PathReporter.report(BulkCreateAttachmentsRequestRt.decode([]))).toStrictEqual([ - 'No errors!', - ]); + comment: 'x', }); + expect(result.success).toBe(false); }); }); +}); - describe('BulkGetAttachmentsRequestRt', () => { - it('has expected attributes in request', () => { - const query = BulkGetAttachmentsRequestRt.decode({ ids: ['abc', 'xyz'] }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ids: ['abc', 'xyz'] }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = BulkGetAttachmentsRequestRt.decode({ ids: ['abc', 'xyz'], foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ids: ['abc', 'xyz'] }, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = BulkGetAttachmentsRequestSchema.safeParse({ ids: ['abc', 'xyz'] }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ids: ['abc', 'xyz'] }); - }); - - it('zod: strips unknown fields', () => { - const result = BulkGetAttachmentsRequestSchema.safeParse({ - ids: ['abc', 'xyz'], - foo: 'bar', - }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ids: ['abc', 'xyz'] }); +describe('AttachmentPatchRequestSchema', () => { + it('requires id and version on top of a full type body', () => { + const result = AttachmentPatchRequestSchema.safeParse({ + ...validUserComment, + id: 'a', + version: 'b', }); + expect(result.success).toBe(true); }); - describe('BulkGetAttachmentsResponseRt', () => { - const defaultRequest = { - attachments: [ - { - comment: 'Solve this fast!', - type: AttachmentType.user, - owner: 'cases', - id: 'basic-comment-id', - version: 'WzQ3LDFc', - created_at: '2020-02-19T23:06:33.798Z', - created_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, - }, - ], - errors: [ - { - error: 'error', - message: 'not found', - status: 404, - savedObjectId: 'abc', - }, - ], - }; - - it('has expected attributes in request', () => { - const query = BulkGetAttachmentsResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('rejects a body missing id', () => { + const result = AttachmentPatchRequestSchema.safeParse({ + ...validUserComment, + version: 'b', }); + expect(result.success).toBe(false); + }); - it('removes foo:bar attributes from request', () => { - const query = BulkGetAttachmentsResponseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('rejects a body missing version', () => { + const result = AttachmentPatchRequestSchema.safeParse({ + ...validUserComment, + id: 'a', }); + expect(result.success).toBe(false); + }); - it('removes foo:bar attributes from attachments', () => { - const query = BulkGetAttachmentsResponseRt.decode({ - ...defaultRequest, - attachments: [{ ...defaultRequest.attachments[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('rejects a body missing the type / payload (partial updates not allowed)', () => { + const result = AttachmentPatchRequestSchema.safeParse({ + id: 'a', + version: 'b', + comment: 'just an update', + owner: 'cases', }); + expect(result.success).toBe(false); + }); - it('removes foo:bar attributes from errors', () => { - const query = BulkGetAttachmentsResponseRt.decode({ - ...defaultRequest, - errors: [{ ...defaultRequest.errors[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('honors the type-specific validation on top of id/version', () => { + const result = AttachmentPatchRequestSchema.safeParse({ + ...validUserComment, + comment: '', + id: 'a', + version: 'b', }); + expect(result.success).toBe(false); + }); - it('zod: has expected attributes in request', () => { - const result = BulkGetAttachmentsResponseSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('DeepStrict-wrapped schema rejects unknown top-level fields on patch (route-layer parity)', () => { + const result = DeepStrict(AttachmentPatchRequestSchema).safeParse({ + ...validUserComment, + id: 'a', + version: 'b', + rogue: 'field', }); + expect(result.success).toBe(false); + }); +}); - it('zod: strips unknown fields', () => { - const result = BulkGetAttachmentsResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); +describe('BulkCreateAttachmentsRequestSchema', () => { + it('accepts an empty array', () => { + expect(BulkCreateAttachmentsRequestSchema.safeParse([]).success).toBe(true); }); - describe('PostFileAttachmentRequestRt', () => { - const defaultRequest = { - file: 'Solve this fast!', - filename: 'filename', - }; + it('accepts a mix of valid attachment variants', () => { + expect( + BulkCreateAttachmentsRequestSchema.safeParse([validUserComment, validAlert, validActions]) + .success + ).toBe(true); + }); - it('has the expected attributes in request', () => { - const query = PostFileAttachmentRequestRt.decode(defaultRequest); + it(`rejects more than MAX_BULK_CREATE_ATTACHMENTS (${MAX_BULK_CREATE_ATTACHMENTS}) items`, () => { + const items = Array(MAX_BULK_CREATE_ATTACHMENTS + 1).fill(validUserComment); + expect(errors(BulkCreateAttachmentsRequestSchema, items)).toContain( + `The length of the field attachments is too long. Array must be of length <= ${MAX_BULK_CREATE_ATTACHMENTS}.` + ); + }); +}); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); +describe('BulkDeleteFileAttachmentsRequestSchema', () => { + it('accepts a single non-empty id', () => { + expect(BulkDeleteFileAttachmentsRequestSchema.safeParse({ ids: ['file-1'] }).success).toBe( + true + ); + }); - it('removes foo:bar attributes from request', () => { - const query = PostFileAttachmentRequestRt.decode({ ...defaultRequest, foo: 'bar' }); + it('rejects an empty ids array', () => { + expect(errors(BulkDeleteFileAttachmentsRequestSchema, { ids: [] })).toContain( + 'The length of the field ids is too short. Array must be of length >= 1.' + ); + }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); + it(`rejects more than MAX_DELETE_FILES (${MAX_DELETE_FILES}) ids`, () => { + const ids = Array(MAX_DELETE_FILES + 1).fill('id'); + expect(errors(BulkDeleteFileAttachmentsRequestSchema, { ids })).toContain( + `The length of the field ids is too long. Array must be of length <= ${MAX_DELETE_FILES}.` + ); + }); - it('zod: has expected attributes in request', () => { - const result = PostFileAttachmentRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); + it('rejects an empty-string id (NonEmptyString parity)', () => { + expect(errors(BulkDeleteFileAttachmentsRequestSchema, { ids: [''] })).toContain( + 'string must have length >= 1' + ); + }); - it('zod: strips unknown fields', () => { - const result = PostFileAttachmentRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); + it('rejects a whitespace-only id (NonEmptyString parity)', () => { + expect(errors(BulkDeleteFileAttachmentsRequestSchema, { ids: [' '] })).toContain( + 'string must have length >= 1' + ); + }); +}); - describe('errors', () => { - it('throws an error when the filename is too long', () => { - const longFilename = 'x'.repeat(MAX_FILENAME_LENGTH + 1); +describe('BulkGetAttachmentsRequestSchema', () => { + it('accepts a single id', () => { + expect(BulkGetAttachmentsRequestSchema.safeParse({ ids: ['a'] }).success).toBe(true); + }); - expect( - PathReporter.report( - PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: longFilename }) - ) - ).toContain('The length of the filename is too long. The maximum length is 160.'); - }); + it('rejects an empty ids array', () => { + expect(errors(BulkGetAttachmentsRequestSchema, { ids: [] })).toContain( + 'The length of the field ids is too short. Array must be of length >= 1.' + ); + }); - it('throws an error when the filename is too small', () => { - expect( - PathReporter.report( - PostFileAttachmentRequestRt.decode({ ...defaultRequest, filename: '' }) - ) - ).toContain('The filename field cannot be an empty string.'); - }); - }); + it(`rejects more than MAX_BULK_GET_ATTACHMENTS (${MAX_BULK_GET_ATTACHMENTS}) ids`, () => { + const ids = Array(MAX_BULK_GET_ATTACHMENTS + 1).fill('id'); + expect(errors(BulkGetAttachmentsRequestSchema, { ids })).toContain( + `The length of the field ids is too long. Array must be of length <= ${MAX_BULK_GET_ATTACHMENTS}.` + ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts index e6b945186f0b2..091a5a6e31d78 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v1.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { MAX_BULK_CREATE_ATTACHMENTS, MAX_BULK_GET_ATTACHMENTS, @@ -21,26 +21,27 @@ import { paginationSchema, } from '../../../schema'; import { - UserCommentAttachmentPayloadRt, - AlertAttachmentPayloadRt, - ActionsAttachmentPayloadRt, - ExternalReferenceNoSOAttachmentPayloadRt, - ExternalReferenceSOAttachmentPayloadRt, - ExternalReferenceSOWithoutRefsAttachmentPayloadRt, - PersistableStateAttachmentPayloadRt, + UserCommentAttachmentPayloadSchema, + AlertAttachmentPayloadSchema, + ActionsAttachmentPayloadSchema, + ExternalReferenceNoSOAttachmentPayloadSchema, + ExternalReferenceSOAttachmentPayloadSchema, + ExternalReferenceSOWithoutRefsAttachmentPayloadSchema, + PersistableStateAttachmentPayloadSchema, AttachmentType, - AttachmentRt, - AttachmentsRt, - EventAttachmentPayloadRt, + AttachmentsSchema, + EventAttachmentPayloadSchema, } from '../../domain/attachment/v1'; +export { AttachmentType }; + /** * Files */ const MIN_DELETE_IDS = 1; -export const BulkDeleteFileAttachmentsRequestRt = rt.strict({ +export const BulkDeleteFileAttachmentsRequestSchema = z.object({ ids: limitedArraySchema({ codec: NonEmptyString, min: MIN_DELETE_IDS, @@ -49,129 +50,119 @@ export const BulkDeleteFileAttachmentsRequestRt = rt.strict({ }), }); -export const PostFileAttachmentRequestRt = rt.intersection([ - rt.strict({ - file: rt.unknown, - }), - rt.exact( - rt.partial({ - filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }), - }) - ), -]); - -export type BulkDeleteFileAttachmentsRequest = rt.TypeOf; -export type PostFileAttachmentRequest = rt.TypeOf; +export const PostFileAttachmentRequestSchema = z.object({ + file: z.unknown(), + filename: limitedStringSchema({ + fieldName: 'filename', + min: 1, + max: MAX_FILENAME_LENGTH, + }).optional(), +}); /** * Attachments */ -const BasicAttachmentRequestRt = rt.union([ - UserCommentAttachmentPayloadRt, - AlertAttachmentPayloadRt, - ActionsAttachmentPayloadRt, - ExternalReferenceNoSOAttachmentPayloadRt, - PersistableStateAttachmentPayloadRt, - EventAttachmentPayloadRt, -]); - -export const AttachmentRequestRt = rt.union([ - rt.strict({ +export const AttachmentRequestSchema = z.union([ + z.object({ comment: limitedStringSchema({ fieldName: 'comment', min: 1, max: MAX_COMMENT_LENGTH }), - type: rt.literal(AttachmentType.user), - owner: rt.string, + type: z.literal(AttachmentType.user), + owner: z.string(), }), - EventAttachmentPayloadRt, - AlertAttachmentPayloadRt, - rt.strict({ - type: rt.literal(AttachmentType.actions), + EventAttachmentPayloadSchema, + AlertAttachmentPayloadSchema, + z.object({ + type: z.literal(AttachmentType.actions), comment: limitedStringSchema({ fieldName: 'comment', min: 1, max: MAX_COMMENT_LENGTH }), - actions: rt.strict({ - targets: rt.array( - rt.strict({ - hostname: rt.string, - endpointId: rt.string, + actions: z.object({ + targets: z.array( + z.object({ + hostname: z.string(), + endpointId: z.string(), }) ), - type: rt.string, + type: z.string(), }), - owner: rt.string, + owner: z.string(), }), - ExternalReferenceNoSOAttachmentPayloadRt, - ExternalReferenceSOAttachmentPayloadRt, - PersistableStateAttachmentPayloadRt, + ExternalReferenceNoSOAttachmentPayloadSchema, + ExternalReferenceSOAttachmentPayloadSchema, + PersistableStateAttachmentPayloadSchema, ]); -export const AttachmentRequestWithoutRefsRt = rt.union([ - BasicAttachmentRequestRt, - ExternalReferenceSOWithoutRefsAttachmentPayloadRt, +export const AttachmentRequestWithoutRefsSchema = z.union([ + UserCommentAttachmentPayloadSchema, + AlertAttachmentPayloadSchema, + EventAttachmentPayloadSchema, + ActionsAttachmentPayloadSchema, + ExternalReferenceNoSOAttachmentPayloadSchema, + ExternalReferenceSOWithoutRefsAttachmentPayloadSchema, + PersistableStateAttachmentPayloadSchema, ]); -export const AttachmentPatchRequestRt = rt.intersection([ +/** + * Partial updates are not allowed. + * We want to prevent the user for changing the type without removing invalid fields. + * injectAttachmentSOAttributesFromRefsForPatch is dependent on this assumption. + * The consumers of the persistable attachment service should always get the + * persistableStateAttachmentState on a patch. + */ +export const AttachmentPatchRequestSchema = AttachmentRequestSchema.and( + z.object({ id: z.string(), version: z.string() }) +); + +export const AttachmentsFindResponseSchema = z.object({ + comments: AttachmentsSchema, + page: z.number(), + per_page: z.number(), + total: z.number(), +}); + +export const FindAttachmentsQueryParamsSchema = paginationSchema({ + maxPerPage: MAX_COMMENTS_PER_PAGE, +}).extend({ /** - * Partial updates are not allowed. - * We want to prevent the user for changing the type without removing invalid fields. - * - * injectAttachmentSOAttributesFromRefsForPatch is dependent on this assumption. - * The consumers of the persistable attachment service should always get the - * persistableStateAttachmentState on a patch. + * Order to sort the response */ - AttachmentRequestRt, - rt.strict({ id: rt.string, version: rt.string }), -]); - -export const AttachmentsFindResponseRt = rt.strict({ - comments: rt.array(AttachmentRt), - page: rt.number, - per_page: rt.number, - total: rt.number, + sortOrder: z.enum(['desc', 'asc']).optional(), }); -export const FindAttachmentsQueryParamsRt = rt.intersection([ - rt.exact( - rt.partial({ - /** - * Order to sort the response - */ - sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - }) - ), - paginationSchema({ maxPerPage: MAX_COMMENTS_PER_PAGE }), -]); - -export const BulkCreateAttachmentsRequestRt = limitedArraySchema({ - codec: AttachmentRequestRt, +export const BulkCreateAttachmentsRequestSchema = limitedArraySchema({ + codec: AttachmentRequestSchema, min: 0, max: MAX_BULK_CREATE_ATTACHMENTS, fieldName: 'attachments', }); -export const BulkGetAttachmentsRequestRt = rt.strict({ +export const BulkGetAttachmentsRequestSchema = z.object({ ids: limitedArraySchema({ - codec: rt.string, + codec: z.string(), min: 1, max: MAX_BULK_GET_ATTACHMENTS, fieldName: 'ids', }), }); -export const BulkGetAttachmentsResponseRt = rt.strict({ - attachments: AttachmentsRt, - errors: rt.array( - rt.strict({ - error: rt.string, - message: rt.string, - status: rt.union([rt.undefined, rt.number]), - savedObjectId: rt.string, +export const BulkGetAttachmentsResponseSchema = z.object({ + attachments: AttachmentsSchema, + errors: z.array( + z.object({ + error: z.string(), + message: z.string(), + status: z.number().optional(), + savedObjectId: z.string(), }) ), }); -export type FindAttachmentsQueryParams = rt.TypeOf; -export type AttachmentsFindResponse = rt.TypeOf; -export type AttachmentRequest = rt.TypeOf; -export type AttachmentPatchRequest = rt.TypeOf; -export type BulkCreateAttachmentsRequest = rt.TypeOf; -export type BulkGetAttachmentsResponse = rt.TypeOf; -export type BulkGetAttachmentsRequest = rt.TypeOf; +export type BulkDeleteFileAttachmentsRequest = z.infer< + typeof BulkDeleteFileAttachmentsRequestSchema +>; +export type PostFileAttachmentRequest = z.infer; +export type AttachmentRequest = z.infer; +export type AttachmentPatchRequest = z.infer; +export type AttachmentsFindResponse = z.infer; +export type FindAttachmentsQueryParams = z.infer; +export type BulkCreateAttachmentsRequest = z.infer; +export type BulkGetAttachmentsRequest = z.infer; +export type BulkGetAttachmentsResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.test.ts index b751de2c7a117..73c26db2ab291 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.test.ts @@ -6,27 +6,19 @@ */ import { AttachmentType } from '../../domain/attachment/v1'; -import { AttachmentRequestRtV2, BulkCreateAttachmentsRequestRtV2 } from './v2'; -import { - AttachmentRequestSchemaV2, - BulkCreateAttachmentsRequestSchemaV2, -} from '../../api_zod/attachment/v2'; +import { AttachmentRequestSchemaV2, BulkCreateAttachmentsRequestSchemaV2 } from './v2'; describe('Unified Attachments', () => { - describe('AttachmentRequestRtV2', () => { + describe('AttachmentRequestSchemaV2', () => { it('accepts v1 user comment attachment request', () => { const v1Request = { comment: 'This is a comment', type: AttachmentType.user, owner: 'cases', }; - - const query = AttachmentRequestRtV2.decode(v1Request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: v1Request, - }); + const result = AttachmentRequestSchemaV2.safeParse(v1Request); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(v1Request); }); it('accepts v2 unified attachment request', () => { @@ -35,348 +27,87 @@ describe('Unified Attachments', () => { attachmentId: 'attachment-123', owner: 'cases', data: { - attributes: { - title: 'My Visualization', - visualizationType: 'lens', - }, - timeRange: { - from: 'now-1d', - to: 'now', - }, - }, - metadata: { - description: 'A test visualization', + attributes: { title: 'My Visualization', visualizationType: 'lens' }, + timeRange: { from: 'now-1d', to: 'now' }, }, + metadata: { description: 'A test visualization' }, }; - - const query = AttachmentRequestRtV2.decode(v2Request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: v2Request, - }); + const result = AttachmentRequestSchemaV2.safeParse(v2Request); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(v2Request); }); it('accepts v2 unified attachment request with only attachmentId', () => { - const v2Request = { - type: 'lens', - owner: 'cases', - attachmentId: 'attachment-123', - }; - - const query = AttachmentRequestRtV2.decode(v2Request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: v2Request, - }); + const v2Request = { type: 'lens', owner: 'cases', attachmentId: 'attachment-123' }; + const result = AttachmentRequestSchemaV2.safeParse(v2Request); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(v2Request); }); it('accepts v2 unified attachment request with only data', () => { - const v2Request = { - type: 'user', - owner: 'cases', - data: { - content: { - title: 'My comment', - }, - }, - }; - - const query = AttachmentRequestRtV2.decode(v2Request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: v2Request, - }); - }); - - it('rejects v2 unified attachment request with neither attachmentId nor data', () => { - const v2Request = { - type: 'lens', - owner: 'cases', - }; - - const query = AttachmentRequestRtV2.decode(v2Request); - - expect(query._tag).toBe('Left'); - }); - - it('removes foo:bar attributes from v1 request', () => { - const v1Request = { - comment: 'This is a comment', - type: AttachmentType.user, - owner: 'cases', - foo: 'bar', - }; - - const query = AttachmentRequestRtV2.decode(v1Request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - comment: 'This is a comment', - type: AttachmentType.user, - owner: 'cases', - }, - }); - }); - - it('removes foo:bar attributes from v2 request', () => { const v2Request = { type: 'lens', - attachmentId: 'attachment-123', owner: 'cases', - data: { - attributes: { - title: 'My Visualization', - }, - }, - metadata: { - description: 'A test visualization', - }, - foo: 'bar', - }; - - const query = AttachmentRequestRtV2.decode(v2Request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - type: 'lens', - attachmentId: 'attachment-123', - owner: 'cases', - data: { - attributes: { - title: 'My Visualization', - }, - }, - metadata: { - description: 'A test visualization', - }, - }, - }); - }); - - it('accepts v1 request even with extra v2 fields (v1 type ignores extra fields)', () => { - const requestWithExtraFields = { - comment: 'This is a comment', - type: AttachmentType.user, - owner: 'cases', - attachmentId: 'attachment-123', - data: { - content: 'My comment', - }, + data: { attributes: { title: 'Viz' } }, }; - - const query = AttachmentRequestRtV2.decode(requestWithExtraFields); - - expect(query._tag).toBe('Right'); - if (query._tag === 'Right') { - // v1 type matches and strips extra fields - expect(query.right).toMatchObject({ - comment: 'This is a comment', - type: AttachmentType.user, - owner: 'cases', - }); - expect(query.right).not.toHaveProperty('attachmentId'); - expect(query.right).not.toHaveProperty('data'); - } + const result = AttachmentRequestSchemaV2.safeParse(v2Request); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(v2Request); }); - it('zod: accepts v1 user comment attachment request', () => { + it('strips unknown fields from v1 request', () => { const v1Request = { comment: 'This is a comment', type: AttachmentType.user, owner: 'cases', }; - const result = AttachmentRequestSchemaV2.safeParse(v1Request); + const result = AttachmentRequestSchemaV2.safeParse({ ...v1Request, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(v1Request); }); + }); - it('zod: accepts v2 unified attachment request', () => { - const v2Request = { - type: 'lens', - attachmentId: 'attachment-123', - owner: 'cases', - }; - const result = AttachmentRequestSchemaV2.safeParse(v2Request); + describe('BulkCreateAttachmentsRequestSchemaV2', () => { + it('accepts array of v1 attachment requests', () => { + const v1Requests = [{ comment: 'First comment', type: AttachmentType.user, owner: 'cases' }]; + const result = BulkCreateAttachmentsRequestSchemaV2.safeParse(v1Requests); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(v2Request); + expect(result.data).toStrictEqual(v1Requests); }); - }); - describe('BulkCreateAttachmentsRequestRtV2', () => { it('accepts empty array', () => { - const query = BulkCreateAttachmentsRequestRtV2.decode([]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [], - }); - }); - - it('accepts array of v1 attachment requests', () => { - const v1Requests = [ - { - comment: 'First comment', - type: AttachmentType.user, - owner: 'cases', - }, - { - comment: 'Second comment', - type: AttachmentType.user, - owner: 'cases', - }, - ]; - - const query = BulkCreateAttachmentsRequestRtV2.decode(v1Requests); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: v1Requests, - }); + const result = BulkCreateAttachmentsRequestSchemaV2.safeParse([]); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual([]); }); - it('accepts array of v2 attachment requests', () => { + it('accepts array of v2 unified attachment requests', () => { const v2Requests = [ - { - type: 'lens', - attachmentId: 'attachment-1', - owner: 'cases', - data: { - attributes: { - title: 'First Visualization', - }, - }, - }, - { - type: 'lens', - attachmentId: 'attachment-2', - owner: 'cases', - data: { - attributes: { - title: 'Second Visualization', - }, - }, - }, - ]; - - const query = BulkCreateAttachmentsRequestRtV2.decode(v2Requests); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: v2Requests, - }); - }); - - it('accepts security.event reference payloads with attachmentId and metadata', () => { - const securityEventRequests = [ - { - type: 'security.event', - attachmentId: 'doc-id', - owner: 'securitySolution', - metadata: { index: '.siem-signals-index' }, - }, + { type: 'lens', attachmentId: 'attachment-1', owner: 'cases' }, + { type: 'lens', attachmentId: 'attachment-2', owner: 'cases' }, ]; - - const query = BulkCreateAttachmentsRequestRtV2.decode(securityEventRequests); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: securityEventRequests, - }); + const result = BulkCreateAttachmentsRequestSchemaV2.safeParse(v2Requests); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(v2Requests); }); it('accepts mixed array of v1 and v2 attachment requests', () => { const mixedRequests = [ - { - comment: 'This is a comment', - type: AttachmentType.user, - owner: 'cases', - }, - { - type: 'lens', - attachmentId: 'attachment-123', - owner: 'cases', - data: { - attributes: { - title: 'My Visualization', - }, - }, - metadata: { - description: 'A test visualization', - }, - }, + { comment: 'A v1 comment', type: AttachmentType.user, owner: 'cases' }, + { type: 'lens', attachmentId: 'attachment-123', owner: 'cases' }, ]; - - const query = BulkCreateAttachmentsRequestRtV2.decode(mixedRequests); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: mixedRequests, - }); - }); - - it('removes foo:bar attributes from requests in array', () => { - const requestsWithExtraFields = [ - { - comment: 'This is a comment', - type: AttachmentType.user, - owner: 'cases', - foo: 'bar', - }, - { - type: 'user', - attachmentId: 'attachment-123', - data: { - content: 'My comment', - }, - owner: 'cases', - foo: 'bar', - }, - ]; - - const query = BulkCreateAttachmentsRequestRtV2.decode(requestsWithExtraFields); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [ - { - comment: 'This is a comment', - type: AttachmentType.user, - owner: 'cases', - }, - { - type: 'user', - attachmentId: 'attachment-123', - data: { - content: 'My comment', - }, - owner: 'cases', - }, - ], - }); - }); - - it('zod: accepts array of v1 attachment requests', () => { - const v1Requests = [ - { - comment: 'First comment', - type: AttachmentType.user, - owner: 'cases', - }, - ]; - const result = BulkCreateAttachmentsRequestSchemaV2.safeParse(v1Requests); + const result = BulkCreateAttachmentsRequestSchemaV2.safeParse(mixedRequests); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(v1Requests); + expect(result.data).toStrictEqual(mixedRequests); }); - it('zod: accepts empty array', () => { - const result = BulkCreateAttachmentsRequestSchemaV2.safeParse([]); + it('strips unknown fields from requests in array', () => { + const requests = [{ comment: 'First comment', type: AttachmentType.user, owner: 'cases' }]; + const result = BulkCreateAttachmentsRequestSchemaV2.safeParse([ + { ...requests[0], foo: 'bar' }, + ]); expect(result.success).toBe(true); - expect(result.data).toStrictEqual([]); + expect(result.data).toStrictEqual(requests); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.ts b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.ts index 361d043da70db..baac25550f68c 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/attachment/v2.ts @@ -5,65 +5,73 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { MAX_BULK_CREATE_ATTACHMENTS } from '../../../constants'; +import { limitedArraySchema } from '../../../schema'; import type { BulkGetAttachmentsRequest } from './v1'; import { - AttachmentPatchRequestRt, - AttachmentRequestRt, - AttachmentRequestWithoutRefsRt, + AttachmentRequestSchema, + AttachmentRequestWithoutRefsSchema, + AttachmentPatchRequestSchema, } from './v1'; + +export type { BulkGetAttachmentsRequest as BulkGetAttachmentsRequestV2 }; import { - AttachmentRtV2, - AttachmentsRtV2, - UnifiedAttachmentPayloadRt, + AttachmentSchemaV2, + AttachmentsSchemaV2, + UnifiedAttachmentPayloadSchema, + UnifiedReferenceAttachmentPayloadSchema, + UnifiedValueAttachmentPayloadSchema, } from '../../domain/attachment/v2'; -import { limitedArraySchema } from '../../../schema'; -export type { BulkGetAttachmentsRequest as BulkGetAttachmentsRequestV2 }; -export const UnifiedAttachmentPatchRequestRt = rt.intersection([ - UnifiedAttachmentPayloadRt, - rt.strict({ id: rt.string, version: rt.string }), +export const UnifiedAttachmentPatchRequestSchema = z.union([ + UnifiedReferenceAttachmentPayloadSchema.extend({ id: z.string(), version: z.string() }), + UnifiedValueAttachmentPayloadSchema.extend({ id: z.string(), version: z.string() }), ]); -export const AttachmentRequestRtV2 = rt.union([AttachmentRequestRt, UnifiedAttachmentPayloadRt]); -export const AttachmentRequestWithoutRefsRtV2 = rt.union([ - AttachmentRequestWithoutRefsRt, - UnifiedAttachmentPayloadRt, +export const AttachmentRequestSchemaV2 = z.union([ + AttachmentRequestSchema, + UnifiedAttachmentPayloadSchema, ]); -export const AttachmentPatchRequestRtV2 = rt.union([ - AttachmentPatchRequestRt, - UnifiedAttachmentPatchRequestRt, + +export const AttachmentRequestWithoutRefsSchemaV2 = z.union([ + AttachmentRequestWithoutRefsSchema, + UnifiedAttachmentPayloadSchema, ]); -export const AttachmentsFindResponseRtV2 = rt.strict({ - comments: rt.array(AttachmentRtV2), - page: rt.number, - per_page: rt.number, - total: rt.number, -}); +export const AttachmentPatchRequestSchemaV2 = z.union([ + AttachmentPatchRequestSchema, + UnifiedAttachmentPatchRequestSchema, +]); -export const BulkCreateAttachmentsRequestRtV2 = limitedArraySchema({ - codec: AttachmentRequestRtV2, +export const BulkCreateAttachmentsRequestSchemaV2 = limitedArraySchema({ + codec: AttachmentRequestSchemaV2, min: 0, max: MAX_BULK_CREATE_ATTACHMENTS, fieldName: 'attachments', }); -export const BulkGetAttachmentsResponseRtV2 = rt.strict({ - attachments: AttachmentsRtV2, - errors: rt.array( - rt.strict({ - error: rt.string, - message: rt.string, - status: rt.union([rt.undefined, rt.number]), - savedObjectId: rt.string, +export const AttachmentsFindResponseSchemaV2 = z.object({ + comments: z.array(AttachmentSchemaV2), + page: z.number(), + per_page: z.number(), + total: z.number(), +}); + +export const BulkGetAttachmentsResponseSchemaV2 = z.object({ + attachments: AttachmentsSchemaV2, + errors: z.array( + z.object({ + error: z.string(), + message: z.string(), + status: z.union([z.undefined(), z.number()]), + savedObjectId: z.string(), }) ), }); -export type AttachmentRequestV2 = rt.TypeOf; -export type AttachmentPatchRequestV2 = rt.TypeOf; -export type AttachmentsFindResponseV2 = rt.TypeOf; -export type BulkCreateAttachmentsRequestV2 = rt.TypeOf; -export type BulkGetAttachmentsResponseV2 = rt.TypeOf; +export type AttachmentRequestV2 = z.infer; +export type AttachmentPatchRequestV2 = z.infer; +export type BulkCreateAttachmentsRequestV2 = z.infer; +export type AttachmentsFindResponseV2 = z.infer; +export type BulkGetAttachmentsResponseV2 = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.test.ts index 5db7956f86ca0..52a8101637efb 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.test.ts @@ -6,120 +6,57 @@ */ import { - CASE_EXTENDED_FIELDS, - MAX_CATEGORY_FILTER_LENGTH, - MAX_TAGS_FILTER_LENGTH, MAX_ASSIGNEES_FILTER_LENGTH, - MAX_REPORTERS_FILTER_LENGTH, MAX_ASSIGNEES_PER_CASE, - MAX_DESCRIPTION_LENGTH, - MAX_TAGS_PER_CASE, - MAX_LENGTH_PER_TAG, - MAX_TITLE_LENGTH, + MAX_CASES_PER_PAGE, + MAX_CASES_TO_UPDATE, + MAX_CATEGORY_FILTER_LENGTH, MAX_CATEGORY_LENGTH, MAX_CUSTOM_FIELDS_PER_CASE, MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH, + MAX_DESCRIPTION_LENGTH, + MAX_LENGTH_PER_TAG, + MAX_REPORTERS_FILTER_LENGTH, + MAX_TAGS_FILTER_LENGTH, + MAX_TAGS_PER_CASE, + MAX_TITLE_LENGTH, } from '../../../constants'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { AttachmentType } from '../../domain/attachment/v1'; -import type { Case } from '../../domain/case/v1'; +import type { ZodType } from '@kbn/zod/v4'; +import { DeepStrict } from '@kbn/zod-helpers'; import { CaseSeverity, CaseStatuses } from '../../domain/case/v1'; import { ConnectorTypes } from '../../domain/connector/v1'; -import { CasesStatusRequestRt, CasesStatusResponseRt } from '../stats/v1'; -import type { CasePostRequest } from './v1'; -import { - AllReportersFindRequestRt, - CasePatchRequestRt, - CasePostRequestRt, - CasePushRequestParamsRt, - CaseResolveResponseRt, - CasesBulkGetRequestRt, - CasesBulkGetResponseRt, - CasesByAlertIDRequestRt, - CasesFindRequestRt, - CasesFindRequestSearchFieldsRt, - CasesFindRequestSortFieldsRt, - CasesFindResponseRt, - CasesPatchRequestRt, - CasesSearchRequestRt, -} from './v1'; import { CustomFieldTypes } from '../../domain/custom_field/v1'; +import type { CasePostRequest } from './v1'; import { - AllReportersFindRequestSchema, CasePatchRequestSchema, CasePostRequestSchema, - CasePushRequestParamsSchema, - CaseResolveResponseSchema, - CasesBulkGetRequestSchema, - CasesBulkGetResponseSchema, - CasesByAlertIDRequestSchema, CasesFindRequestSchema, - CasesFindResponseSchema, CasesPatchRequestSchema, CasesSearchRequestSchema, -} from '../../api_zod/case/v1'; -import { CasesStatusRequestSchema, CasesStatusResponseSchema } from '../../api_zod/stats/v1'; +} from './v1'; -const basicCase: Case = { - owner: 'cases', - closed_at: null, - closed_by: null, - id: 'basic-case-id', - comments: [ - { - comment: 'Solve this fast!', - type: AttachmentType.user, - id: 'basic-comment-id', - created_at: '2020-02-19T23:06:33.798Z', - created_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, - owner: 'cases', - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, - version: 'WzQ3LDFc', - }, - ], - created_at: '2020-02-19T23:06:33.798Z', - created_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, +const errors = (schema: ZodType, value: unknown): string[] => { + const result = schema.safeParse(value); + if (result.success) return []; + return result.error.issues.map((i) => i.message); +}; + +const validPostRequest: CasePostRequest = { + description: 'A description', + tags: ['new', 'case'], + title: 'My new case', connector: { - id: 'none', - name: 'My Connector', - type: ConnectorTypes.none, - fields: null, + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, }, - description: 'Security banana Issue', - severity: CaseSeverity.LOW, - duration: null, - external_service: null, - status: CaseStatuses.open, - tags: ['coke', 'pepsi'], - title: 'Another horrible breach!!', - totalComment: 1, - totalAlerts: 0, - totalEvents: 0, - updated_at: '2020-02-20T15:02:57.995Z', - updated_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, - version: 'WzQ3LDFd', settings: { syncAlerts: true, - extractObservables: false, }, - // damaged_raccoon uid + owner: 'cases', + severity: CaseSeverity.LOW, assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], - category: null, customFields: [ { key: 'first_custom_field_key', @@ -133,1125 +70,320 @@ const basicCase: Case = { }, { key: 'third_custom_field_key', - type: CustomFieldTypes.TEXT, - value: 'www.example.com', - }, - { - key: 'fourth_custom_field_key', type: CustomFieldTypes.NUMBER, value: 3, }, ], - observables: [ - { - value: 'test', - typeKey: '9b557398-0289-4e00-b696-5b277608789c', - id: 'df927ab8-54ed-47d6-be07-9948c255c097', - createdAt: '2024-11-14', - updatedAt: '2024-11-14', - description: null, - }, - ], - total_observables: 1, - incremental_id: 123, }; -describe('CasePostRequestRt', () => { - const defaultRequest: CasePostRequest = { - description: 'A description', - tags: ['new', 'case'], - title: 'My new case', - connector: { - id: '123', - name: 'My connector', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }, - settings: { - syncAlerts: true, - extractObservables: undefined, - }, - owner: 'cases', - severity: CaseSeverity.LOW, - assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], - customFields: [ - { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: 'this is a text field value', - }, - { - key: 'second_custom_field_key', - type: CustomFieldTypes.TOGGLE, - value: true, - }, - { - key: 'third_custom_field_key', - type: CustomFieldTypes.NUMBER, - value: 3, - }, - ], - }; - - it('has expected attributes in request', () => { - const query = CasePostRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); +describe('CasePostRequestSchema', () => { + it('accepts a valid request', () => { + const result = CasePostRequestSchema.safeParse(validPostRequest); + expect(result.success).toBe(true); + if (result.success) expect(result.data).toEqual(validPostRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CasePostRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown top-level fields', () => { + const result = CasePostRequestSchema.safeParse({ ...validPostRequest, foo: 'bar' }); + expect(result.success).toBe(true); + if (result.success) expect(result.data).not.toHaveProperty('foo'); }); - it('removes foo:bar attributes from connector', () => { - const query = CasePostRequestRt.decode({ - ...defaultRequest, - connector: { ...defaultRequest.connector, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields from connector', () => { + const result = CasePostRequestSchema.safeParse({ + ...validPostRequest, + connector: { ...validPostRequest.connector, foo: 'bar' }, }); + expect(result.success).toBe(true); + if (result.success) { + expect((result.data.connector as Record).foo).toBeUndefined(); + } }); - it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => { + it('rejects more than MAX_ASSIGNEES_PER_CASE assignees', () => { const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' }); - - expect( - PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees })) - ).toContain('The length of the field assignees is too long. Array must be of length <= 10.'); + expect(errors(CasePostRequestSchema, { ...validPostRequest, assignees })).toContain( + `The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_PER_CASE}.` + ); }); - it('does not throw an error with empty assignees', async () => { - expect( - PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, assignees: [] })) - ).toContain('No errors!'); + it('accepts an empty assignees array', () => { + const result = CasePostRequestSchema.safeParse({ ...validPostRequest, assignees: [] }); + expect(result.success).toBe(true); }); - it('does not throw an error with undefined assignees', async () => { - const { assignees, ...rest } = defaultRequest; - - expect(PathReporter.report(CasePostRequestRt.decode(rest))).toContain('No errors!'); + it('accepts undefined assignees', () => { + const { assignees, ...rest } = validPostRequest; + expect(CasePostRequestSchema.safeParse(rest).success).toBe(true); }); - it(`throws an error when the description contains more than ${MAX_DESCRIPTION_LENGTH} characters`, async () => { + it('rejects description exceeding MAX_DESCRIPTION_LENGTH', () => { const description = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1); + expect(errors(CasePostRequestSchema, { ...validPostRequest, description })).toContain( + `The length of the description is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}.` + ); + }); - expect( - PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, description })) - ).toContain('The length of the description is too long. The maximum length is 30000.'); + it('rejects whitespace-only description (limitedStringSchema parity)', () => { + expect(errors(CasePostRequestSchema, { ...validPostRequest, description: ' ' })).toContain( + 'The description field cannot be an empty string.' + ); }); - it(`throws an error when there are more than ${MAX_TAGS_PER_CASE} tags`, async () => { + it('rejects more than MAX_TAGS_PER_CASE tags', () => { const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foobar'); - - expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags }))).toContain( - 'The length of the field tags is too long. Array must be of length <= 200.' + expect(errors(CasePostRequestSchema, { ...validPostRequest, tags })).toContain( + `The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_PER_CASE}.` ); }); - it(`throws an error when the a tag is more than ${MAX_LENGTH_PER_TAG} characters`, async () => { - const tag = 'a'.repeat(MAX_LENGTH_PER_TAG + 1); - - expect( - PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, tags: [tag] })) - ).toContain('The length of the tag is too long. The maximum length is 256.'); + it('rejects a tag exceeding MAX_LENGTH_PER_TAG', () => { + const tags = ['a'.repeat(MAX_LENGTH_PER_TAG + 1)]; + expect(errors(CasePostRequestSchema, { ...validPostRequest, tags })).toContain( + `The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` + ); }); - it(`throws an error when the title contains more than ${MAX_TITLE_LENGTH} characters`, async () => { + it('rejects title exceeding MAX_TITLE_LENGTH', () => { const title = 'a'.repeat(MAX_TITLE_LENGTH + 1); - - expect(PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, title }))).toContain( - 'The length of the title is too long. The maximum length is 160.' + expect(errors(CasePostRequestSchema, { ...validPostRequest, title })).toContain( + `The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}.` ); }); - it(`throws an error when the category contains more than ${MAX_CATEGORY_LENGTH} characters`, async () => { + it('rejects category exceeding MAX_CATEGORY_LENGTH', () => { const category = 'a'.repeat(MAX_CATEGORY_LENGTH + 1); - - expect( - PathReporter.report(CasePostRequestRt.decode({ ...defaultRequest, category })) - ).toContain('The length of the category is too long. The maximum length is 50.'); - }); - - it('removes foo:bar attributes from customFields', () => { - const customField = { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: 'this is a text field value', - }; - - const query = CasePostRequestRt.decode({ - ...defaultRequest, - customFields: [{ ...customField, foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, customFields: [{ ...customField }] }, - }); - }); - - it('removes foo:bar attributes from field inside customFields', () => { - const customField = { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: 'this is a text field value', - }; - - const query = CasePostRequestRt.decode({ - ...defaultRequest, - customFields: [{ ...customField, foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, customFields: [{ ...customField }] }, - }); + expect(errors(CasePostRequestSchema, { ...validPostRequest, category })).toContain( + `The length of the category is too long. The maximum length is ${MAX_CATEGORY_LENGTH}.` + ); }); - it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { + it('rejects more than MAX_CUSTOM_FIELDS_PER_CASE customFields', () => { const customFields = Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({ - key: 'first_custom_field_key', + key: 'k', type: CustomFieldTypes.TEXT, - value: 'this is a text field value', + value: 'v', }); - - expect( - PathReporter.report( - CasePostRequestRt.decode({ - ...defaultRequest, - customFields, - }) - ) - ).toContain( + expect(errors(CasePostRequestSchema, { ...validPostRequest, customFields })).toContain( `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.` ); }); - it('does not throw an error with undefined customFields', async () => { - const { customFields, ...rest } = defaultRequest; - - expect(PathReporter.report(CasePostRequestRt.decode(rest))).toContain('No errors!'); - }); - - it('accepts optional template and extended_fields', () => { - const request = { - ...defaultRequest, - template: { id: 'template-id', version: 1 }, - [CASE_EXTENDED_FIELDS]: { field1: 'foo' }, - }; - - const query = CasePostRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); - }); - - it('removes unknown attributes from template', () => { - const request = { - ...defaultRequest, - template: { id: 'template-id', version: 1, foo: 'bar' }, - }; - - const query = CasePostRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - template: { id: 'template-id', version: 1 }, + it('rejects a TEXT customField value exceeding MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH', () => { + const customFields = [ + { + key: 'k', + type: CustomFieldTypes.TEXT, + value: 'a'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), }, - }); + ]; + const result = CasePostRequestSchema.safeParse({ ...validPostRequest, customFields }); + expect(result.success).toBe(false); }); - it(`throws an error when a text customFields is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { - expect( - PathReporter.report( - CasePostRequestRt.decode({ - ...defaultRequest, - customFields: [ - { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), - }, - ], - }) - ) - ).toContain( - `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` - ); - }); - - 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('rejects an empty TEXT customField value', () => { + const customFields = [{ key: 'k', type: CustomFieldTypes.TEXT, value: '' }]; + const result = CasePostRequestSchema.safeParse({ ...validPostRequest, customFields }); + expect(result.success).toBe(false); }); - 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( - CasePostRequestRt.decode({ - ...defaultRequest, - customFields: [ - { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: '', - }, - ], - }) - ) - ).toContain('The value field cannot be an empty string.'); + it('rejects a NUMBER customField value above Number.MAX_SAFE_INTEGER', () => { + const customFields = [ + { key: 'k', type: CustomFieldTypes.NUMBER, value: Number.MAX_SAFE_INTEGER + 1 }, + ]; + const result = CasePostRequestSchema.safeParse({ ...validPostRequest, customFields }); + expect(result.success).toBe(false); }); - it('zod: has expected attributes in request', () => { - // Zod strips keys with undefined values, so omit extractObservables: undefined - const zodRequest = { ...defaultRequest, settings: { syncAlerts: true } }; - const result = CasePostRequestSchema.safeParse(zodRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(zodRequest); + it('rejects a NUMBER customField value below Number.MIN_SAFE_INTEGER', () => { + const customFields = [ + { key: 'k', type: CustomFieldTypes.NUMBER, value: Number.MIN_SAFE_INTEGER - 1 }, + ]; + const result = CasePostRequestSchema.safeParse({ ...validPostRequest, customFields }); + expect(result.success).toBe(false); }); - it('zod: strips unknown fields', () => { - const zodRequest = { ...defaultRequest, settings: { syncAlerts: true } }; - const result = CasePostRequestSchema.safeParse({ ...zodRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(zodRequest); - }); -}); - -describe('CasesFindRequestRt', () => { - const defaultRequest = { - tags: ['new', 'case'], - status: CaseStatuses.open, - severity: CaseSeverity.LOW, - assignees: ['damaged_racoon'], - reporters: ['damaged_racoon'], - defaultSearchOperator: 'AND', - from: 'now', - page: '1', - perPage: '10', - search: 'search text', - searchFields: ['title', 'description', 'incremental_id.text'], - to: '1w', - sortOrder: 'desc', - sortField: 'createdAt', - owner: 'cases', - }; - - it('has expected attributes in request', () => { - const query = CasesFindRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, page: 1, perPage: 10 }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesFindRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, page: 1, perPage: 10 }, - }); - }); - - const searchFields = Object.keys(CasesFindRequestSearchFieldsRt.keys); - - it.each(searchFields)('succeeds with %s as searchFields', (field) => { - const query = CasesFindRequestRt.decode({ ...defaultRequest, searchFields: field }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, searchFields: field, page: 1, perPage: 10 }, - }); - }); - - const sortFields = Object.keys(CasesFindRequestSortFieldsRt.keys); - - it.each(sortFields)('succeeds with %s as sortField', (sortField) => { - const query = CasesFindRequestRt.decode({ ...defaultRequest, sortField }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, sortField, page: 1, perPage: 10 }, + it('DeepStrict-wrapped schema rejects unknown top-level fields (route-layer parity)', () => { + const result = DeepStrict(CasePostRequestSchema).safeParse({ + ...validPostRequest, + foo: 'bar', }); + expect(result.success).toBe(false); }); - it('removes rootSearchField when passed', () => { - expect( - PathReporter.report( - CasesFindRequestRt.decode({ ...defaultRequest, rootSearchField: ['foobar'] }) - ) - ).toContain('No errors!'); - }); - - it('zod: has expected attributes in request', () => { - const result = CasesFindRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ...defaultRequest, page: 1, perPage: 10 }); - }); - - it('zod: strips unknown fields', () => { - const result = CasesFindRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ...defaultRequest, page: 1, perPage: 10 }); - }); - - describe('errors', () => { - it('throws error when invalid searchField passed', () => { - expect( - PathReporter.report( - CasesFindRequestRt.decode({ ...defaultRequest, searchFields: 'foobar' }) - ) - ).not.toContain('No errors!'); - }); - - it('throws error when invalid sortField passed', () => { - expect( - PathReporter.report(CasesFindRequestRt.decode({ ...defaultRequest, sortField: 'foobar' })) - ).not.toContain('No errors!'); - }); - - it('succeeds when valid parameters passed', () => { - expect(PathReporter.report(CasesFindRequestRt.decode(defaultRequest))).toContain( - 'No errors!' - ); - }); - - it(`throws an error when the category array has ${MAX_CATEGORY_FILTER_LENGTH} items`, async () => { - const category = Array(MAX_CATEGORY_FILTER_LENGTH + 1).fill('foobar'); - - expect(PathReporter.report(CasesFindRequestRt.decode({ category }))).toContain( - 'The length of the field category is too long. Array must be of length <= 100.' - ); - }); - - it(`throws an error when the tags array has ${MAX_TAGS_FILTER_LENGTH} items`, async () => { - const tags = Array(MAX_TAGS_FILTER_LENGTH + 1).fill('foobar'); - - expect(PathReporter.report(CasesFindRequestRt.decode({ tags }))).toContain( - 'The length of the field tags is too long. Array must be of length <= 100.' - ); - }); - - it(`throws an error when the assignees array has ${MAX_ASSIGNEES_FILTER_LENGTH} items`, async () => { - const assignees = Array(MAX_ASSIGNEES_FILTER_LENGTH + 1).fill('foobar'); - - expect(PathReporter.report(CasesFindRequestRt.decode({ assignees }))).toContain( - 'The length of the field assignees is too long. Array must be of length <= 100.' - ); - }); - - it(`throws an error when the reporters array has ${MAX_REPORTERS_FILTER_LENGTH} items`, async () => { - const reporters = Array(MAX_REPORTERS_FILTER_LENGTH + 1).fill('foobar'); - - expect(PathReporter.report(CasesFindRequestRt.decode({ reporters }))).toContain( - 'The length of the field reporters is too long. Array must be of length <= 100.' - ); - }); + it('rejects missing required fields', () => { + const { description, ...rest } = validPostRequest; + expect(CasePostRequestSchema.safeParse(rest).success).toBe(false); }); }); -describe('CasesSearchRequestRt', () => { - const defaultRequest = { - tags: ['new', 'case'], - status: CaseStatuses.open, - severity: CaseSeverity.LOW, - assignees: ['damaged_racoon'], - reporters: ['damaged_racoon'], - defaultSearchOperator: 'AND', - from: 'now', - page: '1', - perPage: '10', - search: 'search text', - searchFields: [ - 'cases.title', - 'cases.description', - 'cases.incremental_id.text', - 'cases.observables.value', - 'cases.customFields.value', - 'cases-comments.comment', - 'cases-comments.alertId', - 'cases-comments.eventId', - ], - to: '1w', - sortOrder: 'desc', - sortField: 'createdAt', - owner: 'cases', - customFields: { - toggle_custom_field_key: [true], - another_custom_field: [null, false], - text_custom_field: ['hello'], - number_custom_field: [1234], - }, +describe('CasePatchRequestSchema', () => { + const validPatch = { + id: 'abc-123', + version: 'WzQ3LDFc', + title: 'Updated title', }; - it('has expected attributes in request', () => { - const query = CasesSearchRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, page: 1, perPage: 10 }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesSearchRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, page: 1, perPage: 10 }, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = CasesSearchRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ...defaultRequest, page: 1, perPage: 10 }); - }); - - it('zod: strips unknown fields', () => { - const result = CasesSearchRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ...defaultRequest, page: 1, perPage: 10 }); - }); -}); - -describe('Status', () => { - describe('CasesStatusRequestRt', () => { - const defaultRequest = { - from: '2022-04-28T15:18:00.000Z', - to: '2022-04-28T15:22:00.000Z', - owner: 'cases', - }; - - it('has expected attributes in request', () => { - const query = CasesStatusRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('has removes foo:bar attributes from request', () => { - const query = CasesStatusRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = CasesStatusRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = CasesStatusRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); + it('accepts a valid partial update with id and version', () => { + expect(CasePatchRequestSchema.safeParse(validPatch).success).toBe(true); }); - describe('CasesStatusResponseRt', () => { - const defaultResponse = { - count_closed_cases: 1, - count_in_progress_cases: 2, - count_open_cases: 1, - }; - - it('has expected attributes in response', () => { - const query = CasesStatusResponseRt.decode(defaultResponse); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultResponse, - }); - }); - - it('removes foo:bar attributes from response', () => { - const query = CasesStatusResponseRt.decode({ ...defaultResponse, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultResponse, - }); - }); - - it('zod: has expected attributes in response', () => { - const result = CasesStatusResponseSchema.safeParse(defaultResponse); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultResponse); - }); - - it('zod: strips unknown fields', () => { - const result = CasesStatusResponseSchema.safeParse({ ...defaultResponse, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultResponse); - }); - }); -}); - -describe('CasesByAlertIDRequestRt', () => { - it('has expected attributes in request', () => { - const query = CasesByAlertIDRequestRt.decode({ owner: 'cases' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { owner: 'cases' }, - }); + it('rejects missing id', () => { + const { id, ...rest } = validPatch; + expect(CasePatchRequestSchema.safeParse(rest).success).toBe(false); }); - it('removes foo:bar attributes from request', () => { - const query = CasesByAlertIDRequestRt.decode({ owner: ['cases'], foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { owner: ['cases'] }, - }); + it('rejects missing version', () => { + const { version, ...rest } = validPatch; + expect(CasePatchRequestSchema.safeParse(rest).success).toBe(false); }); - it('zod: has expected attributes in request', () => { - const result = CasesByAlertIDRequestSchema.safeParse({ owner: 'cases' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ owner: 'cases' }); + it('accepts id and version with no other fields', () => { + expect(CasePatchRequestSchema.safeParse({ id: 'a', version: 'b' }).success).toBe(true); }); - it('zod: strips unknown fields', () => { - const result = CasesByAlertIDRequestSchema.safeParse({ owner: ['cases'], foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ owner: ['cases'] }); + it('rejects whitespace-only title (length-bounded fields enforce trim parity)', () => { + expect(errors(CasePatchRequestSchema, { ...validPatch, title: ' ' })).toContain( + 'The title field cannot be an empty string.' + ); }); }); -describe('CaseResolveResponseRt', () => { - const defaultRequest = { - case: { ...basicCase }, - outcome: 'exactMatch', - alias_target_id: 'sample-target-id', - alias_purpose: 'savedObjectConversion', - }; - - it('has expected attributes in request', () => { - const query = CaseResolveResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CaseResolveResponseRt.decode({ ...defaultRequest, foo: 'bar' }); +describe('CasesPatchRequestSchema', () => { + const validPatch = { id: 'abc-123', version: 'WzQ3LDFc', title: 'Updated' }; - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('accepts a non-empty cases array', () => { + expect(CasesPatchRequestSchema.safeParse({ cases: [validPatch] }).success).toBe(true); }); - it('zod: has expected attributes in request', () => { - const result = CaseResolveResponseSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('rejects an empty cases array', () => { + expect(errors(CasesPatchRequestSchema, { cases: [] })).toContain( + 'The length of the field cases is too short. Array must be of length >= 1.' + ); }); - it('zod: strips unknown fields', () => { - const result = CaseResolveResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it(`rejects more than ${MAX_CASES_TO_UPDATE} cases`, () => { + const cases = Array(MAX_CASES_TO_UPDATE + 1).fill(validPatch); + expect(errors(CasesPatchRequestSchema, { cases })).toContain( + `The length of the field cases is too long. Array must be of length <= ${MAX_CASES_TO_UPDATE}.` + ); }); }); -describe('CasesFindResponseRt', () => { - const defaultRequest = { - cases: [{ ...basicCase }], - page: 1, - per_page: 10, - total: 20, - count_open_cases: 10, - count_in_progress_cases: 5, - count_closed_cases: 5, - }; - - it('has expected attributes in request', () => { - const query = CasesFindResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesFindResponseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); +describe('CasesFindRequestSchema', () => { + it('accepts an empty body (all filters optional)', () => { + expect(CasesFindRequestSchema.safeParse({}).success).toBe(true); }); - it('removes foo:bar attributes from cases', () => { - const query = CasesFindResponseRt.decode({ - ...defaultRequest, - cases: [{ ...basicCase, foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('accepts numeric strings for page/perPage (NumberFromString parity)', () => { + expect(CasesFindRequestSchema.safeParse({ page: '1', perPage: '20' }).success).toBe(true); }); - it('zod: has expected attributes in request', () => { - const result = CasesFindResponseSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('rejects non-numeric strings for page/perPage (NumberFromString parity)', () => { + const result = CasesFindRequestSchema.safeParse({ page: 'a', perPage: 'b' }); + expect(result.success).toBe(false); + if (!result.success) { + const messages = result.error.issues.map((i) => i.message); + expect(messages).toEqual( + expect.arrayContaining(['cannot parse to a number', 'cannot parse to a number']) + ); + } }); - it('zod: strips unknown fields', () => { - const result = CasesFindResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it(`rejects perPage above MAX_CASES_PER_PAGE (${MAX_CASES_PER_PAGE})`, () => { + expect(errors(CasesFindRequestSchema, { perPage: MAX_CASES_PER_PAGE + 1 })).toContain( + `The provided perPage value is too high. The maximum allowed perPage value is ${MAX_CASES_PER_PAGE}.` + ); }); -}); - -describe('CasePatchRequestRt', () => { - const defaultRequest = { - id: 'basic-case-id', - version: 'WzQ3LDFd', - description: 'Updated description', - customFields: [ - { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: 'this is a text field value', - }, - { - key: 'second_custom_field_key', - type: 'toggle', - value: true, - }, - { - key: 'third_custom_field_key', - type: 'number', - value: 123, - }, - ], - }; - it('has expected attributes in request', () => { - const query = CasePatchRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('accepts both array and string forms of tags / status / severity / assignees / reporters / owner', () => { + const arrayForm = CasesFindRequestSchema.safeParse({ + tags: ['a', 'b'], + status: [CaseStatuses.open, CaseStatuses.closed], + severity: [CaseSeverity.LOW, CaseSeverity.HIGH], + assignees: ['u1', 'u2'], + reporters: ['r1'], + owner: ['cases'], }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasePatchRequestRt.decode({ ...defaultRequest, foo: 'bar' }); + expect(arrayForm.success).toBe(true); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + const stringForm = CasesFindRequestSchema.safeParse({ + tags: 'a', + status: CaseStatuses.open, + severity: CaseSeverity.LOW, + assignees: 'u1', + reporters: 'r1', + owner: 'cases', }); + expect(stringForm.success).toBe(true); }); - it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => { - const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' }); - - expect( - PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, assignees })) - ).toContain('The length of the field assignees is too long. Array must be of length <= 10.'); - }); - - it('does not throw an error with empty assignees', async () => { - expect( - PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, assignees: [] })) - ).toContain('No errors!'); - }); - - it('does not throw an error with undefined assignees', async () => { - expect(PathReporter.report(CasePatchRequestRt.decode(defaultRequest))).toContain('No errors!'); - }); - - it(`throws an error when the description contains more than ${MAX_DESCRIPTION_LENGTH} characters`, async () => { - const description = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1); - - expect( - PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, description })) - ).toContain('The length of the description is too long. The maximum length is 30000.'); - }); - - it(`throws an error when there are more than ${MAX_TAGS_PER_CASE} tags`, async () => { - const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foobar'); - - expect(PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, tags }))).toContain( - 'The length of the field tags is too long. Array must be of length <= 200.' + it(`rejects category array with ${MAX_CATEGORY_FILTER_LENGTH + 1} items`, () => { + const category = Array(MAX_CATEGORY_FILTER_LENGTH + 1).fill('x'); + expect(errors(CasesFindRequestSchema, { category })).toContain( + `The length of the field category is too long. Array must be of length <= ${MAX_CATEGORY_FILTER_LENGTH}.` ); }); - it(`throws an error when the a tag is more than ${MAX_LENGTH_PER_TAG} characters`, async () => { - const tag = 'a'.repeat(MAX_LENGTH_PER_TAG + 1); - - expect( - PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, tags: [tag] })) - ).toContain('The length of the tag is too long. The maximum length is 256.'); - }); - - it(`throws an error when the title contains more than ${MAX_TITLE_LENGTH} characters`, async () => { - const title = 'a'.repeat(MAX_TITLE_LENGTH + 1); - - expect(PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, title }))).toContain( - 'The length of the title is too long. The maximum length is 160.' + it(`rejects tags array with ${MAX_TAGS_FILTER_LENGTH + 1} items`, () => { + const tags = Array(MAX_TAGS_FILTER_LENGTH + 1).fill('x'); + expect(errors(CasesFindRequestSchema, { tags })).toContain( + `The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_FILTER_LENGTH}.` ); }); - it(`throws an error when the category contains more than ${MAX_CATEGORY_LENGTH} characters`, async () => { - const category = 'a'.repeat(MAX_CATEGORY_LENGTH + 1); - - expect( - PathReporter.report(CasePatchRequestRt.decode({ ...defaultRequest, category })) - ).toContain('The length of the category is too long. The maximum length is 50.'); - }); - - it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { - const customFields = Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({ - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: 'this is a text field value', - }); - - expect( - PathReporter.report( - CasePatchRequestRt.decode({ - ...defaultRequest, - customFields, - }) - ) - ).toContain( - `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.` + it(`rejects assignees array with ${MAX_ASSIGNEES_FILTER_LENGTH + 1} items`, () => { + const assignees = Array(MAX_ASSIGNEES_FILTER_LENGTH + 1).fill('x'); + expect(errors(CasesFindRequestSchema, { assignees })).toContain( + `The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_FILTER_LENGTH}.` ); }); - it(`throws an error when a text customFields is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { - expect( - PathReporter.report( - CasePatchRequestRt.decode({ - ...defaultRequest, - customFields: [ - { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), - }, - ], - }) - ) - ).toContain( - `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` + it(`rejects reporters array with ${MAX_REPORTERS_FILTER_LENGTH + 1} items`, () => { + const reporters = Array(MAX_REPORTERS_FILTER_LENGTH + 1).fill('x'); + expect(errors(CasesFindRequestSchema, { reporters })).toContain( + `The length of the field reporters is too long. Array must be of length <= ${MAX_REPORTERS_FILTER_LENGTH}.` ); }); - it('zod: has expected attributes in request', () => { - const result = CasePatchRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = CasePatchRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); -}); - -describe('CasesPatchRequestRt', () => { - const defaultRequest = { - cases: [ - { - id: 'basic-case-id', - version: 'WzQ3LDFd', - description: 'Updated description', - }, - ], - }; - - it('has expected attributes in request', () => { - const query = CasesPatchRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesPatchRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => { - const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' }); - - expect( - PathReporter.report( - CasesPatchRequestRt.decode({ - cases: [ - { - id: 'basic-case-id', - version: 'WzQ3LDFd', - assignees, - }, - ], - }) - ) - ).toContain('The length of the field assignees is too long. Array must be of length <= 10.'); - }); - - it('zod: has expected attributes in request', () => { - const result = CasesPatchRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = CasesPatchRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); -}); - -describe('CasePushRequestParamsRt', () => { - const defaultRequest = { - case_id: 'basic-case-id', - connector_id: 'basic-connector-id', - }; - - it('has expected attributes in request', () => { - const query = CasePushRequestParamsRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasePushRequestParamsRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = CasePushRequestParamsSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = CasePushRequestParamsSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('rejects an invalid sortField enum value', () => { + expect(CasesFindRequestSchema.safeParse({ sortField: 'badField' }).success).toBe(false); }); -}); - -describe('AllReportersFindRequestRt', () => { - const defaultRequest = { - owner: ['cases', 'security-solution'], - }; - it('has expected attributes in request', () => { - const query = AllReportersFindRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('rejects an invalid searchField enum value', () => { + expect(CasesFindRequestSchema.safeParse({ searchFields: ['badField'] }).success).toBe(false); }); - it('removes foo:bar attributes from request', () => { - const query = AllReportersFindRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = AllReportersFindRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = AllReportersFindRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('DeepStrict-wrapped schema rejects unknown top-level fields (route-layer parity)', () => { + const result = DeepStrict(CasesFindRequestSchema).safeParse({ rootSearchField: 'bad' }); + expect(result.success).toBe(false); }); }); -describe('CasesBulkGetRequestRt', () => { - const defaultRequest = { - ids: ['case-1', 'case-2'], - }; - - it('has expected attributes in request', () => { - const query = CasesBulkGetRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesBulkGetRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, +describe('CasesSearchRequestSchema', () => { + it('accepts the documented searchFields enum values', () => { + const result = CasesSearchRequestSchema.safeParse({ + searchFields: ['cases.description', 'cases-comments.comment'], }); - }); - - it('zod: has expected attributes in request', () => { - const result = CasesBulkGetRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { - const result = CasesBulkGetRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); -}); - -describe('CasesBulkGetResponseRt', () => { - const defaultRequest = { - cases: [basicCase], - errors: [ - { - error: 'error', - message: 'error-message', - status: 403, - caseId: 'basic-case-id', - }, - ], - }; - - it('has expected attributes in request', () => { - const query = CasesBulkGetResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesBulkGetResponseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from cases', () => { - const query = CasesBulkGetResponseRt.decode({ - ...defaultRequest, - cases: [{ ...defaultRequest.cases[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, cases: defaultRequest.cases }, - }); + it('rejects an unsupported searchFields value', () => { + expect(CasesSearchRequestSchema.safeParse({ searchFields: ['cases.unknown'] }).success).toBe( + false + ); }); - it('removes foo:bar attributes from errors', () => { - const query = CasesBulkGetResponseRt.decode({ - ...defaultRequest, - errors: [{ ...defaultRequest.errors[0], foo: 'bar' }], + it('accepts extendedFieldFilters', () => { + const result = CasesSearchRequestSchema.safeParse({ + extendedFieldFilters: [{ label: 'priority', value: 'high' }], }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = CasesBulkGetResponseSchema.safeParse(defaultRequest); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { - const result = CasesBulkGetResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it('rejects extendedFieldFilters with missing fields', () => { + expect( + CasesSearchRequestSchema.safeParse({ extendedFieldFilters: [{ label: 'priority' }] }).success + ).toBe(false); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.ts index 6b88917fb7b43..9dc551de73f2b 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/case/v1.ts @@ -5,482 +5,439 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { - MAX_DESCRIPTION_LENGTH, - MAX_LENGTH_PER_TAG, - MAX_TAGS_PER_CASE, - MAX_TITLE_LENGTH, - MAX_CATEGORY_LENGTH, + CASE_EXTENDED_FIELDS, MAX_ASSIGNEES_FILTER_LENGTH, + MAX_ASSIGNEES_PER_CASE, + MAX_BULK_GET_CASES, MAX_CASES_PER_PAGE, - MAX_DELETE_IDS_LENGTH, - MAX_REPORTERS_FILTER_LENGTH, - MAX_TAGS_FILTER_LENGTH, MAX_CASES_TO_UPDATE, - MAX_BULK_GET_CASES, MAX_CATEGORY_FILTER_LENGTH, - MAX_ASSIGNEES_PER_CASE, + MAX_CATEGORY_LENGTH, MAX_CUSTOM_FIELDS_PER_CASE, - CASE_EXTENDED_FIELDS, + MAX_DELETE_IDS_LENGTH, + MAX_DESCRIPTION_LENGTH, + MAX_LENGTH_PER_TAG, + MAX_REPORTERS_FILTER_LENGTH, + MAX_TAGS_FILTER_LENGTH, + MAX_TAGS_PER_CASE, + MAX_TITLE_LENGTH, } from '../../../constants'; import { - limitedStringSchema, limitedArraySchema, + limitedStringSchema, NonEmptyString, paginationSchema, } from '../../../schema'; import { - CaseCustomFieldToggleRt, - CustomFieldTextTypeRt, - CustomFieldNumberTypeRt, - CaseCloseReasonRt, -} from '../../domain'; + CaseCustomFieldToggleSchema, + CustomFieldTextTypeSchema, + CustomFieldNumberTypeSchema, +} from '../../domain/custom_field/v1'; import { - CaseRt, - CaseSettingsRt, - CaseSeverityRt, - CasesRt, - CaseStatusRt, - CaseTemplate, - RelatedCaseRt, - SimilarCaseRt, + CaseCloseReasonSchema, + CaseSchema, + CasesSchema, + CaseSettingsSchema, + CaseSeveritySchema, + CaseStatusSchema, + CaseTemplateSchema, + RelatedCaseSchema, + SimilarCaseSchema, } from '../../domain/case/v1'; -import { CaseConnectorRt } from '../../domain/connector/v1'; -import { CaseUserProfileRt, UserRt } from '../../domain/user/v1'; -import { CasesStatusResponseRt } from '../stats/v1'; +import { CaseConnectorSchema } from '../../domain/connector/v1'; +import { CaseUserProfileSchema, UserSchema } from '../../domain/user/v1'; +import { CasesStatusResponseSchema } from '../stats/v1'; import { - CaseCustomFieldTextWithValidationValueRt, - CaseCustomFieldNumberWithValidationValueRt, + CaseCustomFieldTextWithValidationValueSchema, + CaseCustomFieldNumberWithValidationValueSchema, } from '../custom_field/v1'; -const CaseCustomFieldTextWithValidationRt = rt.strict({ - key: rt.string, - type: CustomFieldTextTypeRt, - value: rt.union([CaseCustomFieldTextWithValidationValueRt('value'), rt.null]), +const CaseCustomFieldTextWithValidationSchema = z.object({ + key: z.string(), + type: CustomFieldTextTypeSchema, + value: z.union([CaseCustomFieldTextWithValidationValueSchema('value'), z.null()]), }); -const CaseCustomFieldNumberWithValidationRt = rt.strict({ - key: rt.string, - type: CustomFieldNumberTypeRt, - value: rt.union([CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'value' }), rt.null]), +const CaseCustomFieldNumberWithValidationSchema = z.object({ + key: z.string(), + type: CustomFieldNumberTypeSchema, + value: z.union([ + CaseCustomFieldNumberWithValidationValueSchema({ fieldName: 'value' }), + z.null(), + ]), }); -const CustomFieldRt = rt.union([ - CaseCustomFieldTextWithValidationRt, - CaseCustomFieldToggleRt, - CaseCustomFieldNumberWithValidationRt, +const CustomFieldForRequestSchema = z.union([ + CaseCustomFieldTextWithValidationSchema, + CaseCustomFieldToggleSchema, + CaseCustomFieldNumberWithValidationSchema, ]); -export const CaseRequestCustomFieldsRt = limitedArraySchema({ - codec: CustomFieldRt, +export const CaseRequestCustomFieldsSchema = limitedArraySchema({ + codec: CustomFieldForRequestSchema, fieldName: 'customFields', min: 0, max: MAX_CUSTOM_FIELDS_PER_CASE, }); -export const CaseBaseOptionalFieldsRequestRt = rt.exact( - rt.partial({ - /** - * The description of the case - */ - description: limitedStringSchema({ - fieldName: 'description', - min: 1, - max: MAX_DESCRIPTION_LENGTH, - }), - /** - * The identifying strings for filter a case - */ - tags: limitedArraySchema({ - codec: limitedStringSchema({ fieldName: 'tag', min: 1, max: MAX_LENGTH_PER_TAG }), - min: 0, - max: MAX_TAGS_PER_CASE, - fieldName: 'tags', - }), - /** - * The title of a case - */ - title: limitedStringSchema({ fieldName: 'title', min: 1, max: MAX_TITLE_LENGTH }), - /** - * The external system that the case can be synced with - */ - connector: CaseConnectorRt, - /** - * The severity of the case - */ - severity: CaseSeverityRt, - /** - * The users assigned to this case - */ - assignees: limitedArraySchema({ - codec: CaseUserProfileRt, - fieldName: 'assignees', - min: 0, - max: MAX_ASSIGNEES_PER_CASE, - }), - /** - * The category of the case. - */ - category: rt.union([ +export const CaseBaseOptionalFieldsRequestSchema = z.object({ + /** + * The description of the case + */ + description: limitedStringSchema({ + fieldName: 'description', + min: 1, + max: MAX_DESCRIPTION_LENGTH, + }).optional(), + /** + * The identifying strings for filter a case + */ + tags: limitedArraySchema({ + codec: limitedStringSchema({ fieldName: 'tag', min: 1, max: MAX_LENGTH_PER_TAG }), + min: 0, + max: MAX_TAGS_PER_CASE, + fieldName: 'tags', + }).optional(), + /** + * The title of a case + */ + title: limitedStringSchema({ fieldName: 'title', min: 1, max: MAX_TITLE_LENGTH }).optional(), + /** + * The external system that the case can be synced with + */ + connector: CaseConnectorSchema.optional(), + /** + * The severity of the case + */ + severity: CaseSeveritySchema.optional(), + /** + * The users assigned to this case + */ + assignees: limitedArraySchema({ + codec: CaseUserProfileSchema, + fieldName: 'assignees', + min: 0, + max: MAX_ASSIGNEES_PER_CASE, + }).optional(), + /** + * The category of the case. + */ + category: z + .union([ limitedStringSchema({ fieldName: 'category', min: 1, max: MAX_CATEGORY_LENGTH }), - rt.null, - ]), - /** - * Custom fields of the case - */ - customFields: CaseRequestCustomFieldsRt, - /** - * The alert sync settings - */ - settings: CaseSettingsRt, - template: rt.union([CaseTemplate, rt.null]), - [CASE_EXTENDED_FIELDS]: rt.union([rt.undefined, rt.record(rt.string, rt.string)]), - /** - * The close reason to sync to attached alerts - */ - closeReason: CaseCloseReasonRt, - }) -); - -export const CaseRequestFieldsRt = rt.intersection([ - CaseBaseOptionalFieldsRequestRt, - rt.exact( - rt.partial({ - /** - * The current status of the case (open, closed, in-progress) - */ - status: CaseStatusRt, - - /** - * The plugin owner of the case - */ - owner: rt.string, - }) - ), -]); + z.null(), + ]) + .optional(), + /** + * Custom fields of the case + */ + customFields: CaseRequestCustomFieldsSchema.optional(), + /** + * The alert sync settings + */ + settings: CaseSettingsSchema.optional(), + template: CaseTemplateSchema.nullable().optional(), + [CASE_EXTENDED_FIELDS]: z.record(z.string(), z.string()).optional(), + /** + * The close reason to sync to attached alerts + */ + closeReason: CaseCloseReasonSchema.optional(), +}); + +export const CaseRequestFieldsSchema = CaseBaseOptionalFieldsRequestSchema.extend({ + /** + * The current status of the case (open, closed, in-progress) + */ + status: CaseStatusSchema.optional(), + /** + * The plugin owner of the case + */ + owner: z.string().optional(), +}); /** * Create case */ -export const CasePostRequestRt = rt.intersection([ - rt.strict({ - /** - * Description of the case - */ - description: limitedStringSchema({ - fieldName: 'description', - min: 1, - max: MAX_DESCRIPTION_LENGTH, - }), - /** - * Identifiers for the case. - */ - tags: limitedArraySchema({ - codec: limitedStringSchema({ fieldName: 'tag', min: 1, max: MAX_LENGTH_PER_TAG }), - fieldName: 'tags', - min: 0, - max: MAX_TAGS_PER_CASE, - }), - /** - * Title of the case - */ - title: limitedStringSchema({ fieldName: 'title', min: 1, max: MAX_TITLE_LENGTH }), - /** - * The external configuration for the case - */ - connector: CaseConnectorRt, - /** - * Sync settings for alerts - */ - settings: CaseSettingsRt, - /** - * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user - * creating this case must also be granted access to that plugin's feature. - */ - owner: rt.string, +export const CasePostRequestSchema = z.object({ + /** + * Description of the case + */ + description: limitedStringSchema({ + fieldName: 'description', + min: 1, + max: MAX_DESCRIPTION_LENGTH, }), - rt.exact( - rt.partial({ - /** - * The users assigned to the case - */ - assignees: limitedArraySchema({ - codec: CaseUserProfileRt, - fieldName: 'assignees', - min: 0, - max: MAX_ASSIGNEES_PER_CASE, - }), - /** - * The severity of the case. The severity is - * default it to "low" if not provided. - */ - severity: CaseSeverityRt, - /** - * The category of the case. - */ - category: rt.union([ - limitedStringSchema({ fieldName: 'category', min: 1, max: MAX_CATEGORY_LENGTH }), - rt.null, - ]), - /** - * The list of custom field values of the case. - */ - customFields: CaseRequestCustomFieldsRt, - template: rt.union([CaseTemplate, rt.null]), - [CASE_EXTENDED_FIELDS]: rt.record(rt.string, rt.string), - }) - ), -]); + /** + * Identifiers for the case. + */ + tags: limitedArraySchema({ + codec: limitedStringSchema({ fieldName: 'tag', min: 1, max: MAX_LENGTH_PER_TAG }), + fieldName: 'tags', + min: 0, + max: MAX_TAGS_PER_CASE, + }), + /** + * Title of the case + */ + title: limitedStringSchema({ fieldName: 'title', min: 1, max: MAX_TITLE_LENGTH }), + /** + * The external configuration for the case + */ + connector: CaseConnectorSchema, + /** + * Sync settings for alerts + */ + settings: CaseSettingsSchema, + /** + * The owner here must match the string used when a plugin registers a feature with access to the cases plugin. The user + * creating this case must also be granted access to that plugin's feature. + */ + owner: z.string(), + /** + * The users assigned to the case + */ + assignees: limitedArraySchema({ + codec: CaseUserProfileSchema, + fieldName: 'assignees', + min: 0, + max: MAX_ASSIGNEES_PER_CASE, + }).optional(), + /** + * The severity of the case. The severity is + * default it to "low" if not provided. + */ + severity: CaseSeveritySchema.optional(), + /** + * The category of the case. + */ + category: z + .union([ + limitedStringSchema({ fieldName: 'category', min: 1, max: MAX_CATEGORY_LENGTH }), + z.null(), + ]) + .optional(), + /** + * The list of custom field values of the case. + */ + customFields: CaseRequestCustomFieldsSchema.optional(), + template: CaseTemplateSchema.nullable().optional(), + [CASE_EXTENDED_FIELDS]: z.record(z.string(), z.string()).optional(), +}); /** * Bulk create cases */ -const CaseCreateRequestWithOptionalId = rt.intersection([ - CasePostRequestRt, - rt.exact(rt.partial({ id: rt.string })), -]); +const CaseCreateRequestWithOptionalIdSchema = CasePostRequestSchema.extend({ + id: z.string().optional(), +}); -export const BulkCreateCasesRequestRt = rt.strict({ - cases: rt.array(CaseCreateRequestWithOptionalId), +export const BulkCreateCasesRequestSchema = z.object({ + cases: z.array(CaseCreateRequestWithOptionalIdSchema), }); -export const BulkCreateCasesResponseRt = rt.strict({ - cases: rt.array(CaseRt), +export const BulkCreateCasesResponseSchema = z.object({ + cases: z.array(CaseSchema), }); /** * Find cases */ -export const CasesFindRequestSearchFieldsRt = rt.keyof({ - description: null, - title: null, - 'incremental_id.text': null, +const CasesFindRequestSearchFieldsValues = ['description', 'title', 'incremental_id.text'] as const; +const CasesFindRequestSortFieldsValues = [ + 'title', + 'category', + 'createdAt', + 'updatedAt', + 'closedAt', + 'status', + 'severity', +] as const; + +export const CasesFindRequestSearchFieldsSchema = z.enum(CasesFindRequestSearchFieldsValues); +export const CasesFindRequestSortFieldsSchema = z.enum(CasesFindRequestSortFieldsValues); + +const CasesFindRequestBaseFieldsSchema = paginationSchema({ + maxPerPage: MAX_CASES_PER_PAGE, +}).extend({ + /** + * Tags to filter by + */ + tags: z + .union([ + limitedArraySchema({ + codec: z.string(), + fieldName: 'tags', + min: 0, + max: MAX_TAGS_FILTER_LENGTH, + }), + z.string(), + ]) + .optional(), + /** + * The status of the case (open, closed, in-progress) + */ + status: z.union([CaseStatusSchema, z.array(CaseStatusSchema)]).optional(), + /** + * The severity of the case + */ + severity: z.union([CaseSeveritySchema, z.array(CaseSeveritySchema)]).optional(), + /** + * The uids of the user profiles to filter by + */ + assignees: z + .union([ + limitedArraySchema({ + codec: z.string(), + fieldName: 'assignees', + min: 0, + max: MAX_ASSIGNEES_FILTER_LENGTH, + }), + z.string(), + ]) + .optional(), + /** + * The reporters to filter by + */ + reporters: z + .union([ + limitedArraySchema({ + codec: z.string(), + fieldName: 'reporters', + min: 0, + max: MAX_REPORTERS_FILTER_LENGTH, + }), + z.string(), + ]) + .optional(), + /** + * Operator to use for the `search` field + */ + defaultSearchOperator: z.enum(['AND', 'OR']).optional(), + /** + * A KQL date. If used all cases created after (gte) the from date will be returned + */ + from: z.string().optional(), + /** + * An Elasticsearch simple_query_string + */ + search: z.string().optional(), + /** + * The field to use for sorting the found objects. + * + */ + sortField: CasesFindRequestSortFieldsSchema.optional(), + /** + * The order to sort by + */ + sortOrder: z.enum(['desc', 'asc']).optional(), + /** + * A KQL date. If used all cases created before (lte) the to date will be returned. + */ + to: z.string().optional(), + /** + * The owner(s) to filter by. The user making the request must have privileges to retrieve cases of that + * ownership or they will be ignored. If no owner is included, then all ownership types will be included in the response + * that the user has access to. + */ + owner: z.union([z.array(z.string()), z.string()]).optional(), + /** + * The category of the case. + */ + category: z + .union([ + limitedArraySchema({ + codec: z.string(), + fieldName: 'category', + min: 0, + max: MAX_CATEGORY_FILTER_LENGTH, + }), + z.string(), + ]) + .optional(), }); -export const CasesFindRequestSortFieldsRt = rt.keyof({ - title: null, - category: null, - createdAt: null, - updatedAt: null, - closedAt: null, - status: null, - severity: null, +export const CasesFindRequestSchema = CasesFindRequestBaseFieldsSchema.extend({ + /** + * The fields to perform the simple_query_string parsed query against + */ + searchFields: z + .union([z.array(CasesFindRequestSearchFieldsSchema), CasesFindRequestSearchFieldsSchema]) + .optional(), }); -export const CasesFindRequestBaseFieldsRt = rt.intersection([ - rt.exact( - rt.partial({ - /** - * Tags to filter by - */ - tags: rt.union([ - limitedArraySchema({ - codec: rt.string, - fieldName: 'tags', - min: 0, - max: MAX_TAGS_FILTER_LENGTH, - }), - rt.string, - ]), - /** - * The status of the case (open, closed, in-progress) - */ - status: rt.union([CaseStatusRt, rt.array(CaseStatusRt)]), - /** - * The severity of the case - */ - severity: rt.union([CaseSeverityRt, rt.array(CaseSeverityRt)]), - /** - * The uids of the user profiles to filter by - */ - assignees: rt.union([ - limitedArraySchema({ - codec: rt.string, - fieldName: 'assignees', - min: 0, - max: MAX_ASSIGNEES_FILTER_LENGTH, - }), - rt.string, - ]), - /** - * The reporters to filter by - */ - reporters: rt.union([ - limitedArraySchema({ - codec: rt.string, - fieldName: 'reporters', - min: 0, - max: MAX_REPORTERS_FILTER_LENGTH, - }), - rt.string, - ]), - /** - * Operator to use for the `search` field - */ - defaultSearchOperator: rt.union([rt.literal('AND'), rt.literal('OR')]), - /** - * A KQL date. If used all cases created after (gte) the from date will be returned - */ - from: rt.string, - /** - * The page of objects to return - */ - // page: rt.union([rt.number, NumberFromString]), - /** - * The number of objects to include in each page - */ - // perPage: rt.union([rt.number, NumberFromString]), - /** - * An Elasticsearch simple_query_string - */ - search: rt.string, - /** - * The field to use for sorting the found objects. - * - */ - sortField: CasesFindRequestSortFieldsRt, - /** - * The order to sort by - */ - sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - - /** - * A KQL date. If used all cases created before (lte) the to date will be returned. - */ - to: rt.string, - /** - * The owner(s) to filter by. The user making the request must have privileges to retrieve cases of that - * ownership or they will be ignored. If no owner is included, then all ownership types will be included in the response - * that the user has access to. - */ - - owner: rt.union([rt.array(rt.string), rt.string]), - /** - * The category of the case. - */ - category: rt.union([ - limitedArraySchema({ - codec: rt.string, - fieldName: 'category', - min: 0, - max: MAX_CATEGORY_FILTER_LENGTH, - }), - rt.string, - ]), - }) - ), - paginationSchema({ maxPerPage: MAX_CASES_PER_PAGE }), -]); - -export const CasesFindRequestRt = rt.intersection([ - CasesFindRequestBaseFieldsRt, - rt.exact( - rt.partial({ - /** - * The fields to perform the simple_query_string parsed query against - */ - searchFields: rt.union([ - rt.array(CasesFindRequestSearchFieldsRt), - CasesFindRequestSearchFieldsRt, - ]), - }) - ), -]); - -export const CasesFindRequestWithCustomFieldsRt = rt.intersection([ - CasesFindRequestRt, - rt.exact( - rt.partial({ - /** - * custom fields of the case - */ - customFields: rt.record( - rt.string, - rt.array(rt.union([rt.string, rt.boolean, rt.number, rt.null])) - ), - }) - ), -]); - /** * search cases */ -export const CasesSearchRequestSearchFieldsRt = rt.keyof({ - 'cases.description': null, - 'cases.title': null, - 'cases.incremental_id.text': null, - 'cases.observables.value': null, - 'cases.customFields.value': null, - 'cases-comments.comment': null, - 'cases-comments.alertId': null, - 'cases-comments.eventId': null, +const CasesSearchRequestSearchFieldsValues = [ + 'cases.description', + 'cases.title', + 'cases.incremental_id.text', + 'cases.observables.value', + 'cases.customFields.value', + 'cases-comments.comment', + 'cases-comments.alertId', + 'cases-comments.eventId', +] as const; + +export const CasesSearchRequestSearchFieldsSchema = z.enum(CasesSearchRequestSearchFieldsValues); + +const ExtendedFieldFilterSchema = z.object({ + label: z.string(), + value: z.string(), }); -const ExtendedFieldFilterRt = rt.strict({ - label: rt.string, - value: rt.string, +export const CasesSearchRequestSchema = CasesFindRequestBaseFieldsSchema.extend({ + /** + * custom fields of the case + */ + customFields: z + .record(z.string(), z.array(z.union([z.string(), z.boolean(), z.number(), z.null()]))) + .optional(), + /** + * The fields to perform the simple_query_string parsed query against. + */ + searchFields: z + .union([z.array(CasesSearchRequestSearchFieldsSchema), CasesSearchRequestSearchFieldsSchema]) + .optional(), + /** + * Extended field filters parsed from label:value syntax in the search bar. + */ + extendedFieldFilters: z.array(ExtendedFieldFilterSchema).optional(), }); -export const CasesSearchRequestRt = rt.intersection([ - CasesFindRequestBaseFieldsRt, - rt.exact( - rt.partial({ - /** - * custom fields of the case - */ - customFields: rt.record( - rt.string, - rt.array(rt.union([rt.string, rt.boolean, rt.number, rt.null])) - ), - }) - ), - rt.exact( - rt.partial({ - /** - * The fields to perform the simple_query_string parsed query against. - */ - searchFields: rt.union([ - rt.array(CasesSearchRequestSearchFieldsRt), - CasesSearchRequestSearchFieldsRt, - ]), - }) - ), - rt.exact( - rt.partial({ - /** - * Extended field filters parsed from label:value syntax in the search bar. - */ - extendedFieldFilters: rt.array(ExtendedFieldFilterRt), - }) - ), -]); +export const CasesFindRequestWithCustomFieldsSchema = CasesFindRequestSchema.extend({ + /** + * custom fields of the case + */ + customFields: z + .record(z.string(), z.array(z.union([z.string(), z.boolean(), z.number(), z.null()]))) + .optional(), +}); -export const CasesFindResponseRt = rt.intersection([ - rt.strict({ - cases: rt.array(CaseRt), - page: rt.number, - per_page: rt.number, - total: rt.number, - }), - CasesStatusResponseRt, -]); +export const CasesFindResponseSchema = CasesStatusResponseSchema.extend({ + cases: z.array(CaseSchema), + page: z.number(), + per_page: z.number(), + total: z.number(), +}); -export const CasesSimilarResponseRt = rt.strict({ - cases: rt.array(SimilarCaseRt), - page: rt.number, - per_page: rt.number, - total: rt.number, +export const CasesSimilarResponseSchema = z.object({ + cases: z.array(SimilarCaseSchema), + page: z.number(), + per_page: z.number(), + total: z.number(), }); /** * Delete cases */ -export const CasesDeleteRequestRt = limitedArraySchema({ +export const CasesDeleteRequestSchema = limitedArraySchema({ codec: NonEmptyString, min: 1, max: MAX_DELETE_IDS_LENGTH, @@ -491,37 +448,29 @@ export const CasesDeleteRequestRt = limitedArraySchema({ * Resolve case */ -export const CaseResolveResponseRt = rt.intersection([ - rt.strict({ - case: CaseRt, - outcome: rt.union([rt.literal('exactMatch'), rt.literal('aliasMatch'), rt.literal('conflict')]), - }), - rt.exact( - rt.partial({ - alias_target_id: rt.string, - alias_purpose: rt.union([ - rt.literal('savedObjectConversion'), - rt.literal('savedObjectImport'), - ]), - }) - ), -]); +export const CaseResolveResponseSchema = z.object({ + case: CaseSchema, + outcome: z.enum(['exactMatch', 'aliasMatch', 'conflict']), + alias_target_id: z.string().optional(), + alias_purpose: z.enum(['savedObjectConversion', 'savedObjectImport']).optional(), +}); /** * Get cases */ -export const CasesBulkGetRequestRt = rt.strict({ - ids: limitedArraySchema({ codec: rt.string, min: 1, max: MAX_BULK_GET_CASES, fieldName: 'ids' }), + +export const CasesBulkGetRequestSchema = z.object({ + ids: limitedArraySchema({ codec: z.string(), min: 1, max: MAX_BULK_GET_CASES, fieldName: 'ids' }), }); -export const CasesBulkGetResponseRt = rt.strict({ - cases: CasesRt, - errors: rt.array( - rt.strict({ - error: rt.string, - message: rt.string, - status: rt.union([rt.undefined, rt.number]), - caseId: rt.string, +export const CasesBulkGetResponseSchema = z.object({ + cases: CasesSchema, + errors: z.array( + z.object({ + error: z.string(), + message: z.string(), + status: z.number().optional(), + caseId: z.string(), }) ), }); @@ -529,147 +478,129 @@ export const CasesBulkGetResponseRt = rt.strict({ /** * Update cases */ -export const CasePatchRequestRt = rt.intersection([ - CaseRequestFieldsRt, - /** - * The saved object ID and version - */ - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); -export const CasesPatchRequestRt = rt.strict({ +/** + * The saved object ID and version + */ +export const CasePatchRequestSchema = CaseRequestFieldsSchema.extend({ + id: z.string(), + version: z.string(), +}); + +export const CasesPatchRequestSchema = z.object({ cases: limitedArraySchema({ - codec: CasePatchRequestRt, + codec: CasePatchRequestSchema, min: 1, max: MAX_CASES_TO_UPDATE, fieldName: 'cases', }), }); -export const UpdateSummaryRt = rt.strict({ - syncedAlertCount: rt.number, +export const UpdateSummarySchema = z.object({ + syncedAlertCount: z.number(), }); -export const CaseWithUpdateSummaryRt = rt.intersection([ - CaseRt, - rt.partial({ updateSummary: UpdateSummaryRt }), -]); +export const CaseWithUpdateSummarySchema = CaseSchema.extend({ + updateSummary: UpdateSummarySchema.optional(), +}); -export const PatchCasesResponseRt = rt.array(CaseWithUpdateSummaryRt); +export const PatchCasesResponseSchema = z.array(CaseWithUpdateSummarySchema); /** * Push case */ -export const CasePushRequestParamsRt = rt.strict({ - case_id: rt.string, - connector_id: rt.string, +export const CasePushRequestParamsSchema = z.object({ + case_id: z.string(), + connector_id: z.string(), }); /** * Taxonomies */ -export const AllTagsFindRequestRt = rt.exact( - rt.partial({ - /** - * The owner of the cases to retrieve the tags from. If no owner is provided the tags from all cases - * that the user has access to will be returned. - */ - owner: rt.union([rt.array(rt.string), rt.string]), - }) -); - -export const AllCategoriesFindRequestRt = rt.exact( - rt.partial({ - /** - * The owner of the cases to retrieve the categories from. If no owner is provided the categories - * from all cases that the user has access to will be returned. - */ - owner: rt.union([rt.array(rt.string), rt.string]), - }) -); - -export const AllReportersFindRequestRt = AllTagsFindRequestRt; - -export const GetTagsResponseRt = rt.array(rt.string); -export const GetCategoriesResponseRt = rt.array(rt.string); -export const GetReportersResponseRt = rt.array(UserRt); +export const AllTagsFindRequestSchema = z.object({ + /** + * The owner of the cases to retrieve the tags from. If no owner is provided the tags from all cases + * that the user has access to will be returned. + */ + owner: z.union([z.array(z.string()), z.string()]).optional(), +}); + +export const AllCategoriesFindRequestSchema = AllTagsFindRequestSchema; +export const AllReportersFindRequestSchema = AllTagsFindRequestSchema; + +export const GetTagsResponseSchema = z.array(z.string()); +export const GetCategoriesResponseSchema = z.array(z.string()); +export const GetReportersResponseSchema = z.array(UserSchema); /** * Alerts */ -export const CasesByAlertIDRequestRt = rt.exact( - rt.partial({ - /** - * The type of cases to retrieve given an alert ID. If no owner is provided, all cases - * that the user has access to will be returned. - */ - owner: rt.union([rt.array(rt.string), rt.string]), - }) -); - -export const GetRelatedCasesByAlertResponseRt = rt.array(RelatedCaseRt); - -export const SimilarCasesSearchRequestRt = paginationSchema({ maxPerPage: MAX_CASES_PER_PAGE }); - -export const FindCasesContainingAllDocumentsRequestRt = rt.exact( - rt.type({ - /** - * The IDs of the documents to find cases for. - */ - documentIds: rt.union([rt.array(rt.string), rt.undefined]), - /** - * The IDs of the alerts to find cases for. TODO: remove this in the next serverless release cycle https://github.com/elastic/security-team/issues/14718 - */ - alertIds: rt.union([rt.array(rt.string), rt.undefined]), - // The IDs of the cases to find alerts for. - caseIds: rt.array(rt.string), - }) -); - -export const FindCasesContainingAllAlertsResponseRt = rt.exact( - rt.type({ - casesWithAllAttachments: rt.array(rt.string), - }) -); - -export type CasePostRequest = rt.TypeOf; -export type CaseResolveResponse = rt.TypeOf; -export type CasesDeleteRequest = rt.TypeOf; -export type CasesByAlertIDRequest = rt.TypeOf; -export type CasesFindRequest = rt.TypeOf; -export type CasesFindRequestWithCustomFields = rt.TypeOf; -export type CasesSearchRequest = rt.TypeOf; -export type CasesFindRequestSortFields = rt.TypeOf; -export type CasesFindResponse = rt.TypeOf; -export type CasePatchRequest = rt.TypeOf; -export type CasesPatchRequest = rt.TypeOf; -export type UpdateSummary = rt.TypeOf; -export type CaseWithUpdateSummary = rt.TypeOf; -export type CasesPatchResponse = rt.TypeOf; -export type AllTagsFindRequest = rt.TypeOf; -export type GetTagsResponse = rt.TypeOf; -export type AllCategoriesFindRequest = rt.TypeOf; -export type GetCategoriesResponse = rt.TypeOf; +export const CasesByAlertIDRequestSchema = z.object({ + /** + * The type of cases to retrieve given an alert ID. If no owner is provided, all cases + * that the user has access to will be returned. + */ + owner: z.union([z.array(z.string()), z.string()]).optional(), +}); + +export const GetRelatedCasesByAlertResponseSchema = z.array(RelatedCaseSchema); + +export const SimilarCasesSearchRequestSchema = paginationSchema({ maxPerPage: MAX_CASES_PER_PAGE }); + +export const FindCasesContainingAllDocumentsRequestSchema = z.object({ + /** + * The IDs of the documents to find cases for. + */ + documentIds: z.array(z.string()).optional(), + /** + * The IDs of the alerts to find cases for. TODO: remove this in the next serverless release cycle https://github.com/elastic/security-team/issues/14718 + */ + alertIds: z.array(z.string()).optional(), + // The IDs of the cases to find alerts for. + caseIds: z.array(z.string()), +}); + +export const FindCasesContainingAllAlertsResponseSchema = z.object({ + casesWithAllAttachments: z.array(z.string()), +}); + +export type CasePostRequest = z.infer; +export type CaseResolveResponse = z.infer; +export type CasesDeleteRequest = z.infer; +export type CasesByAlertIDRequest = z.infer; +export type CasesFindRequest = z.infer; +export type CasesFindResponse = z.infer; +export type CasePatchRequest = z.infer; +export type CasesPatchRequest = z.infer; +export type UpdateSummary = z.infer; +export type CaseWithUpdateSummary = z.infer; +export type CasesPatchResponse = z.infer; +export type GetTagsResponse = z.infer; +export type GetCategoriesResponse = z.infer; +export type GetReportersResponse = z.infer; +export type CasesBulkGetRequest = z.infer; +export type CasesBulkGetResponse = z.infer; +export type BulkCreateCasesRequest = z.infer; +export type BulkCreateCasesResponse = z.infer; +export type CasesFindRequestSortFields = z.infer; +export type CasesFindRequestWithCustomFields = z.infer< + typeof CasesFindRequestWithCustomFieldsSchema +>; +export type CasesSearchRequest = z.infer; +export type AllTagsFindRequest = z.infer; +export type AllCategoriesFindRequest = z.infer; export type AllReportersFindRequest = AllTagsFindRequest; -export type GetReportersResponse = rt.TypeOf; -export type CasesBulkGetRequest = rt.TypeOf; -export type CasesBulkGetResponse = rt.TypeOf; -export type GetRelatedCasesByAlertResponse = rt.TypeOf; -export type CaseRequestCustomFields = rt.TypeOf; -export type CaseRequestCustomField = rt.TypeOf; -export type BulkCreateCasesRequest = rt.TypeOf; -export type BulkCreateCasesResponse = rt.TypeOf; -export type SimilarCasesSearchRequest = rt.TypeOf; -export type CasesSimilarResponse = rt.TypeOf; -export type FindCasesContainingAllDocumentsRequest = rt.TypeOf< - typeof FindCasesContainingAllDocumentsRequestRt +export type GetRelatedCasesByAlertResponse = z.infer; +export type CaseRequestCustomFields = z.infer; +export type CaseRequestCustomField = z.infer; +export type SimilarCasesSearchRequest = z.infer; +export type CasesSimilarResponse = z.infer; +export type FindCasesContainingAllDocumentsRequest = z.infer< + typeof FindCasesContainingAllDocumentsRequestSchema >; -export type FindCasesContainingAllAlertsResponse = rt.TypeOf< - typeof FindCasesContainingAllAlertsResponseRt +export type FindCasesContainingAllAlertsResponse = z.infer< + typeof FindCasesContainingAllAlertsResponseSchema >; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.test.ts index e5a993436d4ae..4b498a247231c 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; import { v4 as uuidv4 } from 'uuid'; import { MAX_ASSIGNEES_PER_CASE, @@ -31,18 +30,6 @@ import { import { CaseSeverity } from '../../domain'; import { ConnectorTypes } from '../../domain/connector/v1'; import { CustomFieldTypes } from '../../domain/custom_field/v1'; -import { - CaseConfigureRequestParamsRt, - ConfigurationPatchRequestRt, - ConfigurationRequestRt, - GetConfigurationFindRequestRt, - CustomFieldConfigurationWithoutTypeRt, - TextCustomFieldConfigurationRt, - ToggleCustomFieldConfigurationRt, - NumberCustomFieldConfigurationRt, - TemplateConfigurationRt, - ObservableTypesConfigurationRt, -} from './v1'; import { CaseConfigureRequestParamsSchema, ConfigurationPatchRequestSchema, @@ -54,7 +41,7 @@ import { NumberCustomFieldConfigurationSchema, TemplateConfigurationSchema, ObservableTypesConfigurationSchema, -} from '../../api_zod/configure/v1'; +} from './v1'; describe('configure', () => { const serviceNow = { @@ -64,7 +51,7 @@ describe('configure', () => { fields: null, }; - describe('ConfigurationRequestRt', () => { + describe('ConfigurationRequestSchema', () => { const defaultRequest = { connector: serviceNow, closure_type: 'close-by-user', @@ -72,110 +59,29 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = ConfigurationRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('has expected attributes in request with customFields', () => { - const request = { - ...defaultRequest, - customFields: [ - { - key: 'text_custom_field', - label: 'Text custom field', - type: CustomFieldTypes.TEXT, - required: false, - }, - { - key: 'toggle_custom_field', - label: 'Toggle custom field', - type: CustomFieldTypes.TOGGLE, - required: false, - }, - { - key: 'number_custom_field', - label: 'Number custom field', - type: CustomFieldTypes.NUMBER, - required: false, - }, - ], - }; - const query = ConfigurationRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); + const result = ConfigurationRequestSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('has expected attributes in request with observableTypes', () => { - const request = { - ...defaultRequest, - observableTypes: [ - { - key: '371357ae-77ce-44bd-88b7-fbba9c80501f', - label: 'Example Label', - }, - ], - }; - const query = ConfigurationRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); + it('strips unknown fields', () => { + const result = ConfigurationRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { + it(`does not accept customFields exceeding ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { const customFields = new Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({ key: 'text_custom_field', label: 'Text custom field', type: CustomFieldTypes.TEXT, required: false, }); - - expect( - PathReporter.report(ConfigurationRequestRt.decode({ ...defaultRequest, customFields }))[0] - ).toContain( - `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}` - ); - }); - - it('has expected attributes in request with templates', () => { - const request = { - ...defaultRequest, - templates: [ - { - key: 'template_key_1', - name: 'Template 1', - description: 'this is first template', - tags: ['foo', 'bar'], - caseFields: { - title: 'case using sample template', - }, - }, - { - key: 'template_key_2', - name: 'Template 2', - description: 'this is second template', - tags: [], - caseFields: null, - }, - ], - }; - const query = ConfigurationRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); + const result = ConfigurationRequestSchema.safeParse({ ...defaultRequest, customFields }); + expect(result.success).toBe(false); }); - it(`limits templates to ${MAX_TEMPLATES_LENGTH}`, () => { + it(`does not accept templates exceeding ${MAX_TEMPLATES_LENGTH}`, () => { const templates = new Array(MAX_TEMPLATES_LENGTH + 1).fill({ key: 'template_key_1', name: 'Template 1', @@ -184,35 +90,12 @@ describe('configure', () => { title: 'case using sample template', }, }); - - expect( - PathReporter.report(ConfigurationRequestRt.decode({ ...defaultRequest, templates }))[0] - ).toContain(`The length of the field templates is too long. Array must be of length <= 10.`); - }); - - it('removes foo:bar attributes from request', () => { - const query = ConfigurationRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = ConfigurationRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = ConfigurationRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + const result = ConfigurationRequestSchema.safeParse({ ...defaultRequest, templates }); + expect(result.success).toBe(false); }); }); - describe('ConfigurationPatchRequestRt', () => { + describe('ConfigurationPatchRequestSchema', () => { const defaultRequest = { connector: serviceNow, closure_type: 'close-by-user', @@ -220,87 +103,29 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = ConfigurationPatchRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = ConfigurationPatchRequestSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('has expected attributes in request with customFields', () => { - const request = { - ...defaultRequest, - customFields: [ - { - key: 'text_custom_field', - label: 'Text custom field', - type: CustomFieldTypes.TEXT, - required: false, - }, - { - key: 'toggle_custom_field', - label: 'Toggle custom field', - type: CustomFieldTypes.TOGGLE, - required: false, - }, - ], - }; - const query = ConfigurationPatchRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); + it('strips unknown fields', () => { + const result = ConfigurationPatchRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { + it(`does not accept customFields exceeding ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { const customFields = new Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({ key: 'text_custom_field', label: 'Text custom field', type: CustomFieldTypes.TEXT, required: false, }); - - expect( - PathReporter.report( - ConfigurationPatchRequestRt.decode({ ...defaultRequest, customFields }) - )[0] - ).toContain( - `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}` - ); - }); - - it('has expected attributes in request with templates', () => { - const request = { - ...defaultRequest, - templates: [ - { - key: 'template_key_1', - name: 'Template 1', - description: 'this is first template', - tags: ['foo', 'bar'], - caseFields: { - title: 'case using sample template', - }, - }, - { - key: 'template_key_2', - name: 'Template 2', - description: 'this is second template', - caseFields: null, - }, - ], - }; - const query = ConfigurationPatchRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); + const result = ConfigurationPatchRequestSchema.safeParse({ ...defaultRequest, customFields }); + expect(result.success).toBe(false); }); - it(`limits templates to ${MAX_TEMPLATES_LENGTH}`, () => { + it(`does not accept templates exceeding ${MAX_TEMPLATES_LENGTH}`, () => { const templates = new Array(MAX_TEMPLATES_LENGTH + 1).fill({ key: 'template_key_1', name: 'Template 1', @@ -310,125 +135,48 @@ describe('configure', () => { title: 'case using sample template', }, }); - - expect( - PathReporter.report(ConfigurationPatchRequestRt.decode({ ...defaultRequest, templates }))[0] - ).toContain(`The length of the field templates is too long. Array must be of length <= 10.`); - }); - - it('has expected attributes in request with observableTypes', () => { - const request = { - ...defaultRequest, - observableTypes: [ - { - key: '371357ae-77ce-44bd-88b7-fbba9c80501f', - label: 'Example Label', - }, - ], - }; - const query = ConfigurationPatchRequestRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ConfigurationPatchRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = ConfigurationPatchRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = ConfigurationPatchRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + const result = ConfigurationPatchRequestSchema.safeParse({ ...defaultRequest, templates }); + expect(result.success).toBe(false); }); }); - describe('GetConfigurationFindRequestRt', () => { + describe('GetConfigurationFindRequestSchema', () => { const defaultRequest = { owner: ['cases'], }; it('has expected attributes in request', () => { - const query = GetConfigurationFindRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = GetConfigurationFindRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = GetConfigurationFindRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = GetConfigurationFindRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CaseConfigureRequestParamsRt', () => { + describe('CaseConfigureRequestParamsSchema', () => { const defaultRequest = { configuration_id: 'basic-configuration-id', }; it('has expected attributes in request', () => { - const query = CaseConfigureRequestParamsRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CaseConfigureRequestParamsRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = CaseConfigureRequestParamsSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CaseConfigureRequestParamsSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CustomFieldConfigurationWithoutTypeRt', () => { + describe('CustomFieldConfigurationWithoutTypeSchema', () => { const defaultRequest = { key: 'custom_field_key', label: 'Custom field label', @@ -436,92 +184,57 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = CustomFieldConfigurationWithoutTypeRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); + const result = CustomFieldConfigurationWithoutTypeSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, + it('strips unknown fields', () => { + const result = CustomFieldConfigurationWithoutTypeSchema.safeParse({ + ...defaultRequest, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('limits key to 36 characters', () => { + it('does not accept key longer than 36 characters', () => { const longKey = 'x'.repeat(MAX_CUSTOM_FIELD_KEY_LENGTH + 1); - - expect( - PathReporter.report( - CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key: longKey }) - ) - ).toContain('The length of the key is too long. The maximum length is 36.'); + const result = CustomFieldConfigurationWithoutTypeSchema.safeParse({ + ...defaultRequest, + key: longKey, + }); + expect(result.success).toBe(false); }); - it('returns an error if they key is not in the expected format', () => { - const key = 'Not a proper key'; - - expect( - PathReporter.report( - CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key }) - ) - ).toContain(`Key must be lower case, a-z, 0-9, '_', and '-' are allowed`); + it('does not accept key not in expected format', () => { + const result = CustomFieldConfigurationWithoutTypeSchema.safeParse({ + ...defaultRequest, + key: 'Not a proper key', + }); + expect(result.success).toBe(false); }); it('accepts a uuid as a key', () => { const key = uuidv4(); - - const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, key }, - }); - }); - - it('accepts a slug as a key', () => { - const key = 'abc_key-1'; - - const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, key }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, key }, + const result = CustomFieldConfigurationWithoutTypeSchema.safeParse({ + ...defaultRequest, + key, }); - }); - - it('limits label to 50 characters', () => { - const longLabel = 'x'.repeat(MAX_CUSTOM_FIELD_LABEL_LENGTH + 1); - - expect( - PathReporter.report( - CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, label: longLabel }) - ) - ).toContain('The length of the label is too long. The maximum length is 50.'); - }); - - it('zod: has expected attributes in request', () => { - const result = CustomFieldConfigurationWithoutTypeSchema.safeParse(defaultRequest); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('does not accept label longer than 50 characters', () => { + const longLabel = 'x'.repeat(MAX_CUSTOM_FIELD_LABEL_LENGTH + 1); const result = CustomFieldConfigurationWithoutTypeSchema.safeParse({ ...defaultRequest, - foo: 'bar', + label: longLabel, }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + expect(result.success).toBe(false); }); }); - describe('TextCustomFieldConfigurationRt', () => { + describe('TextCustomFieldConfigurationSchema', () => { const defaultRequest = { key: 'my_text_custom_field', label: 'Text Custom Field', @@ -530,87 +243,46 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = TextCustomFieldConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); + const result = TextCustomFieldConfigurationSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('has expected attributes in request with defaultValue', () => { - const query = TextCustomFieldConfigurationRt.decode({ + it('strips unknown fields', () => { + const result = TextCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, - defaultValue: 'foobar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, defaultValue: 'foobar' }, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = TextCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, + it('does not accept defaultValue that is not a string', () => { + const result = TextCustomFieldConfigurationSchema.safeParse({ + ...defaultRequest, + defaultValue: false, }); + expect(result.success).toBe(false); }); - it('defaultValue fails if the type is not string', () => { - expect( - PathReporter.report( - TextCustomFieldConfigurationRt.decode({ - ...defaultRequest, - defaultValue: false, - }) - )[0] - ).toContain('Invalid value false supplied'); - }); - - it(`throws an error if the default value is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { - expect( - PathReporter.report( - TextCustomFieldConfigurationRt.decode({ - ...defaultRequest, - defaultValue: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), - }) - )[0] - ).toContain( - `The length of the defaultValue is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` - ); - }); - - it('throws an error if the default value is an empty string', () => { - expect( - PathReporter.report( - TextCustomFieldConfigurationRt.decode({ - ...defaultRequest, - defaultValue: '', - }) - )[0] - ).toContain('The defaultValue field cannot be an empty string.'); - }); - - it('zod: has expected attributes in request', () => { - const result = TextCustomFieldConfigurationSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it(`does not accept default value longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { + const result = TextCustomFieldConfigurationSchema.safeParse({ + ...defaultRequest, + defaultValue: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), + }); + expect(result.success).toBe(false); }); - it('zod: strips unknown fields', () => { + it('does not accept empty string as default value', () => { const result = TextCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, - foo: 'bar', + defaultValue: '', }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + expect(result.success).toBe(false); }); }); - describe('ToggleCustomFieldConfigurationRt', () => { + describe('ToggleCustomFieldConfigurationSchema', () => { const defaultRequest = { key: 'my_toggle_custom_field', label: 'Toggle Custom Field', @@ -619,42 +291,12 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = ToggleCustomFieldConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ToggleCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('defaultValue fails if the type is not boolean', () => { - expect( - PathReporter.report( - ToggleCustomFieldConfigurationRt.decode({ - ...defaultRequest, - required: true, - defaultValue: 'foobar', - }) - )[0] - ).toContain('Invalid value "foobar" supplied'); - }); - - it('zod: has expected attributes in request', () => { const result = ToggleCustomFieldConfigurationSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ToggleCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -662,9 +304,18 @@ describe('configure', () => { expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); + + it('does not accept defaultValue that is not a boolean', () => { + const result = ToggleCustomFieldConfigurationSchema.safeParse({ + ...defaultRequest, + required: true, + defaultValue: 'foobar', + }); + expect(result.success).toBe(false); + }); }); - describe('NumberCustomFieldConfigurationRt', () => { + describe('NumberCustomFieldConfigurationSchema', () => { const defaultRequest = { key: 'my_number_custom_field', label: 'Number Custom Field', @@ -673,100 +324,54 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = NumberCustomFieldConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); + const result = NumberCustomFieldConfigurationSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('has expected attributes in request with defaultValue', () => { - const query = NumberCustomFieldConfigurationRt.decode({ + it('strips unknown fields', () => { + const result = NumberCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, - defaultValue: 1, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, defaultValue: 1 }, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = NumberCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, + it('does not accept defaultValue that is a string', () => { + const result = NumberCustomFieldConfigurationSchema.safeParse({ + ...defaultRequest, + defaultValue: 'string', }); + expect(result.success).toBe(false); }); - 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.' - ); + it('does not accept defaultValue that is a boolean', () => { + const result = NumberCustomFieldConfigurationSchema.safeParse({ + ...defaultRequest, + defaultValue: false, + }); + expect(result.success).toBe(false); }); - it('zod: has expected attributes in request', () => { - const result = NumberCustomFieldConfigurationSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + it(`does not accept default value more than ${Number.MAX_SAFE_INTEGER}`, () => { + const result = NumberCustomFieldConfigurationSchema.safeParse({ + ...defaultRequest, + defaultValue: Number.MAX_SAFE_INTEGER + 1, + }); + expect(result.success).toBe(false); }); - it('zod: strips unknown fields', () => { + it(`does not accept default value less than ${Number.MIN_SAFE_INTEGER}`, () => { const result = NumberCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, - foo: 'bar', + defaultValue: Number.MIN_SAFE_INTEGER - 1, }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + expect(result.success).toBe(false); }); }); - describe('TemplateConfigurationRt', () => { + describe('TemplateConfigurationSchema', () => { const defaultRequest = { key: 'template_key_1', name: 'Template 1', @@ -778,65 +383,40 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = TemplateConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); + const result = TemplateConfigurationSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = TemplateConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); + it('strips unknown fields', () => { + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('limits key to 36 characters', () => { + it('does not accept key longer than 36 characters', () => { const longKey = 'x'.repeat(MAX_TEMPLATE_KEY_LENGTH + 1); - - expect( - PathReporter.report(TemplateConfigurationRt.decode({ ...defaultRequest, key: longKey })) - ).toContain('The length of the key is too long. The maximum length is 36.'); + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, key: longKey }); + expect(result.success).toBe(false); }); - it('return error if key is empty', () => { - expect( - PathReporter.report(TemplateConfigurationRt.decode({ ...defaultRequest, key: '' })) - ).toContain('The key field cannot be an empty string.'); + it('does not accept empty key', () => { + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, key: '' }); + expect(result.success).toBe(false); }); - it('returns an error if they key is not in the expected format', () => { - const key = 'Not a proper key'; - - expect( - PathReporter.report(TemplateConfigurationRt.decode({ ...defaultRequest, key })) - ).toContain(`Key must be lower case, a-z, 0-9, '_', and '-' are allowed`); - }); - - it('accepts a uuid as an key', () => { - const key = uuidv4(); - - const query = TemplateConfigurationRt.decode({ ...defaultRequest, key }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, key }, + it('does not accept key not in expected format', () => { + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + key: 'Not a proper key', }); + expect(result.success).toBe(false); }); - it('accepts a slug as an key', () => { - const key = 'abc_key-1'; - - const query = TemplateConfigurationRt.decode({ ...defaultRequest, key }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, key }, - }); + it('accepts a uuid as a key', () => { + const key = uuidv4(); + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, key }); + expect(result.success).toBe(true); }); it('does not throw when there is no description or tags', () => { @@ -845,89 +425,68 @@ describe('configure', () => { name: 'Template 1', caseFields: null, }; - - expect(PathReporter.report(TemplateConfigurationRt.decode({ ...newRequest }))).toContain( - 'No errors!' - ); + const result = TemplateConfigurationSchema.safeParse(newRequest); + expect(result.success).toBe(true); }); - it('limits name to 50 characters', () => { + it('does not accept name longer than 50 characters', () => { const longName = 'x'.repeat(MAX_TEMPLATE_NAME_LENGTH + 1); - - expect( - PathReporter.report(TemplateConfigurationRt.decode({ ...defaultRequest, name: longName })) - ).toContain('The length of the name is too long. The maximum length is 50.'); + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, name: longName }); + expect(result.success).toBe(false); }); - it('limits description to 1000 characters', () => { + it('does not accept description longer than 1000 characters', () => { const longDesc = 'x'.repeat(MAX_TEMPLATE_DESCRIPTION_LENGTH + 1); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ ...defaultRequest, description: longDesc }) - ) - ).toContain('The length of the description is too long. The maximum length is 1000.'); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + description: longDesc, + }); + expect(result.success).toBe(false); }); - it(`throws an error when there are more than ${MAX_TAGS_PER_TEMPLATE} tags`, async () => { + it(`does not accept more than ${MAX_TAGS_PER_TEMPLATE} tags`, () => { const tags = Array(MAX_TAGS_PER_TEMPLATE + 1).fill('foobar'); - - expect( - PathReporter.report(TemplateConfigurationRt.decode({ ...defaultRequest, tags })) - ).toContain( - `The length of the field template's tags is too long. Array must be of length <= 10.` - ); + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, tags }); + expect(result.success).toBe(false); }); - it(`throws an error when the a tag is more than ${MAX_TEMPLATE_TAG_LENGTH} characters`, async () => { + it(`does not accept a tag longer than ${MAX_TEMPLATE_TAG_LENGTH} characters`, () => { const tag = 'a'.repeat(MAX_TEMPLATE_TAG_LENGTH + 1); - - expect( - PathReporter.report(TemplateConfigurationRt.decode({ ...defaultRequest, tags: [tag] })) - ).toContain(`The length of the template's tag is too long. The maximum length is 50.`); + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, tags: [tag] }); + expect(result.success).toBe(false); }); - it(`throws an error when the a tag is empty string`, async () => { - expect( - PathReporter.report(TemplateConfigurationRt.decode({ ...defaultRequest, tags: [''] })) - ).toContain(`The template's tag field cannot be an empty string.`); + it('does not accept empty string tag', () => { + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, tags: [''] }); + expect(result.success).toBe(false); }); describe('caseFields', () => { - it('removes foo:bar attributes from caseFields', () => { - const query = TemplateConfigurationRt.decode({ + it('strips unknown fields from caseFields', () => { + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, caseFields: { ...defaultRequest.caseFields, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts caseFields as null', () => { - const query = TemplateConfigurationRt.decode({ + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, caseFields: null, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, caseFields: null }, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ ...defaultRequest, caseFields: null }); }); it('accepts caseFields as {}', () => { - const query = TemplateConfigurationRt.decode({ + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, caseFields: {}, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, caseFields: {} }, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ ...defaultRequest, caseFields: {} }); }); it('accepts caseFields with all fields', () => { @@ -952,199 +511,147 @@ describe('configure', () => { fields: null, }, }; - - const query = TemplateConfigurationRt.decode({ + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, caseFields: caseFieldsAll, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, caseFields: caseFieldsAll }, - }); + expect(result.success).toBe(true); }); - it(`throws an error when the assignees are more than ${MAX_ASSIGNEES_PER_CASE}`, async () => { + it(`does not accept more than ${MAX_ASSIGNEES_PER_CASE} assignees`, () => { const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foobar' }); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { ...defaultRequest.caseFields, assignees }, - }) - ) - ).toContain( - 'The length of the field assignees is too long. Array must be of length <= 10.' - ); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { ...defaultRequest.caseFields, assignees }, + }); + expect(result.success).toBe(false); }); - it(`throws an error when the description contains more than ${MAX_DESCRIPTION_LENGTH} characters`, async () => { + it(`does not accept description longer than ${MAX_DESCRIPTION_LENGTH} characters`, () => { const description = 'a'.repeat(MAX_DESCRIPTION_LENGTH + 1); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { ...defaultRequest.caseFields, description }, - }) - ) - ).toContain('The length of the description is too long. The maximum length is 30000.'); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { ...defaultRequest.caseFields, description }, + }); + expect(result.success).toBe(false); }); - it(`throws an error when there are more than ${MAX_TAGS_PER_CASE} tags`, async () => { + it(`does not accept more than ${MAX_TAGS_PER_CASE} tags`, () => { const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foobar'); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { ...defaultRequest.caseFields, tags }, - }) - ) - ).toContain('The length of the field tags is too long. Array must be of length <= 200.'); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { ...defaultRequest.caseFields, tags }, + }); + expect(result.success).toBe(false); }); - it(`throws an error when the tag is more than ${MAX_LENGTH_PER_TAG} characters`, async () => { + it(`does not accept tag longer than ${MAX_LENGTH_PER_TAG} characters`, () => { const tag = 'a'.repeat(MAX_LENGTH_PER_TAG + 1); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { ...defaultRequest.caseFields, tags: [tag] }, - }) - ) - ).toContain('The length of the tag is too long. The maximum length is 256.'); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { ...defaultRequest.caseFields, tags: [tag] }, + }); + expect(result.success).toBe(false); }); - it(`throws an error when the title contains more than ${MAX_TITLE_LENGTH} characters`, async () => { + it(`does not accept title longer than ${MAX_TITLE_LENGTH} characters`, () => { const title = 'a'.repeat(MAX_TITLE_LENGTH + 1); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { ...defaultRequest.caseFields, title }, - }) - ) - ).toContain('The length of the title is too long. The maximum length is 160.'); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { ...defaultRequest.caseFields, title }, + }); + expect(result.success).toBe(false); }); - it(`throws an error when the category contains more than ${MAX_CATEGORY_LENGTH} characters`, async () => { + it(`does not accept category longer than ${MAX_CATEGORY_LENGTH} characters`, () => { const category = 'a'.repeat(MAX_CATEGORY_LENGTH + 1); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { ...defaultRequest.caseFields, category }, - }) - ) - ).toContain('The length of the category is too long. The maximum length is 50.'); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { ...defaultRequest.caseFields, category }, + }); + expect(result.success).toBe(false); }); - it(`limits customFields to ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { + it(`does not accept customFields exceeding ${MAX_CUSTOM_FIELDS_PER_CASE}`, () => { const customFields = Array(MAX_CUSTOM_FIELDS_PER_CASE + 1).fill({ key: 'first_custom_field_key', type: CustomFieldTypes.TEXT, value: 'this is a text field value', }); - - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { ...defaultRequest.caseFields, customFields }, - }) - ) - ).toContain( - `The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.` - ); + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { ...defaultRequest.caseFields, customFields }, + }); + expect(result.success).toBe(false); }); - it(`throws an error when a text customFields is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { - expect( - PathReporter.report( - TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: { - ...defaultRequest.caseFields, - customFields: [ - { - key: 'first_custom_field_key', - type: CustomFieldTypes.TEXT, - value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), - }, - ], + it(`does not accept a text customField longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { + const result = TemplateConfigurationSchema.safeParse({ + ...defaultRequest, + caseFields: { + ...defaultRequest.caseFields, + customFields: [ + { + key: 'first_custom_field_key', + type: CustomFieldTypes.TEXT, + value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), }, - }) - ) - ).toContain( - `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` - ); + ], + }, + }); + expect(result.success).toBe(false); }); }); - - it('zod: has expected attributes in request', () => { - const result = TemplateConfigurationSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); }); - describe('ObservableTypesConfigurationRt', () => { + describe('ObservableTypesConfigurationSchema', () => { it('should validate a correct observable types configuration', () => { const validData = [ { key: 'observable_key_1', label: 'Observable Label 1' }, { key: 'observable_key_2', label: 'Observable Label 2' }, ]; + const result = ObservableTypesConfigurationSchema.safeParse(validData); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(validData); + }); - const result = ObservableTypesConfigurationRt.decode(validData); - expect(PathReporter.report(result).join()).toContain('No errors!'); + it('strips unknown fields', () => { + const result = ObservableTypesConfigurationSchema.safeParse([ + { key: 'observable_key_1', label: 'Observable Label 1', foo: 'bar' }, + ]); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual([{ key: 'observable_key_1', label: 'Observable Label 1' }]); }); it('should invalidate an observable types configuration with an invalid key', () => { - const invalidData = [{ key: 'Invalid Key!', label: 'Observable Label 1' }]; - - const result = ObservableTypesConfigurationRt.decode(invalidData); - expect(PathReporter.report(result).join()).not.toContain('No errors!'); + const result = ObservableTypesConfigurationSchema.safeParse([ + { key: 'Invalid Key!', label: 'Observable Label 1' }, + ]); + expect(result.success).toBe(false); }); it('should invalidate an observable types configuration with a missing label', () => { - const invalidData = [{ key: 'observable_key_1' }]; - - const result = ObservableTypesConfigurationRt.decode(invalidData); - expect(PathReporter.report(result).join()).not.toContain('No errors!'); + const result = ObservableTypesConfigurationSchema.safeParse([{ key: 'observable_key_1' }]); + expect(result.success).toBe(false); }); - it('should accept an observable types configuration with an empty array', () => { - const invalidData: unknown[] = []; - - const result = ObservableTypesConfigurationRt.decode(invalidData); - expect(PathReporter.report(result).join()).toContain('No errors!'); + it('should accept an empty array', () => { + const result = ObservableTypesConfigurationSchema.safeParse([]); + expect(result.success).toBe(true); }); it('should invalidate an observable types configuration with a label exceeding max length', () => { - const invalidData = [ + const result = ObservableTypesConfigurationSchema.safeParse([ { key: 'observable_key_1', label: 'a'.repeat(MAX_OBSERVABLE_TYPE_LABEL_LENGTH + 1) }, - ]; - - const result = ObservableTypesConfigurationRt.decode(invalidData); - expect(PathReporter.report(result).join()).not.toContain('No errors!'); + ]); + expect(result.success).toBe(false); }); it('should invalidate an observable types configuration with a key exceeding max length', () => { - const invalidData = [{ key: 'a'.repeat(MAX_OBSERVABLE_TYPE_KEY_LENGTH + 1), label: 'label' }]; - - const result = ObservableTypesConfigurationRt.decode(invalidData); - expect(PathReporter.report(result).join()).not.toContain('No errors!'); + const result = ObservableTypesConfigurationSchema.safeParse([ + { key: 'a'.repeat(MAX_OBSERVABLE_TYPE_KEY_LENGTH + 1), label: 'label' }, + ]); + expect(result.success).toBe(false); }); it('should invalidate an observable types configuration with observableTypes count exceeding max', () => { @@ -1152,49 +659,8 @@ describe('configure', () => { key: 'foo', label: 'label', }); - - const result = ObservableTypesConfigurationRt.decode(invalidData); - expect(PathReporter.report(result).join()).not.toContain('No errors!'); - }); - - it('accepts a uuid as an key', () => { - const key = uuidv4(); - - const query = ObservableTypesConfigurationRt.decode([{ key, label: 'Observable Label 1' }]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [{ key, label: 'Observable Label 1' }], - }); - }); - - it('accepts a slug as an key', () => { - const key = 'abc_key-1'; - - const query = ObservableTypesConfigurationRt.decode([{ key, label: 'Observable Label 1' }]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [{ key, label: 'Observable Label 1' }], - }); - }); - - it('zod: has expected attributes in request', () => { - const validData = [ - { key: 'observable_key_1', label: 'Observable Label 1' }, - { key: 'observable_key_2', label: 'Observable Label 2' }, - ]; - const result = ObservableTypesConfigurationSchema.safeParse(validData); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(validData); - }); - - it('zod: strips unknown fields', () => { - const result = ObservableTypesConfigurationSchema.safeParse([ - { key: 'observable_key_1', label: 'Observable Label 1', foo: 'bar' }, - ]); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual([{ key: 'observable_key_1', label: 'Observable Label 1' }]); + const result = ObservableTypesConfigurationSchema.safeParse(invalidData); + expect(result.success).toBe(false); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.ts index e5682d314f726..86ca01e7019c7 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/configure/v1.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { MAX_CUSTOM_FIELDS_PER_CASE, MAX_CUSTOM_FIELD_KEY_LENGTH, @@ -20,26 +20,26 @@ import { MAX_TEMPLATE_NAME_LENGTH, MAX_TEMPLATE_TAG_LENGTH, } from '../../../constants'; -import { limitedArraySchema, limitedStringSchema, regexStringRt } from '../../../schema'; +import { limitedArraySchema, limitedStringSchema, regexStringSchema } from '../../../schema'; 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'; + CustomFieldTextTypeSchema, + CustomFieldToggleTypeSchema, + CustomFieldNumberTypeSchema, +} from '../../domain/custom_field/v1'; +import { ClosureTypeSchema, ConfigurationBasicWithoutOwnerSchema } from '../../domain/configure/v1'; +import type { Configuration, Configurations } from '../../domain/configure/v1'; +import { CaseConnectorSchema } from '../../domain/connector/v1'; +import { CaseBaseOptionalFieldsRequestSchema } from '../case/v1'; import { - CaseCustomFieldTextWithValidationValueRt, - CaseCustomFieldNumberWithValidationValueRt, + CaseCustomFieldTextWithValidationValueSchema, + CaseCustomFieldNumberWithValidationValueSchema, } from '../custom_field/v1'; -export const CustomFieldConfigurationWithoutTypeRt = rt.strict({ +export const CustomFieldConfigurationWithoutTypeSchema = z.object({ /** * key of custom field */ - key: regexStringRt({ + key: regexStringSchema({ codec: limitedStringSchema({ fieldName: 'key', min: 1, max: MAX_CUSTOM_FIELD_KEY_LENGTH }), pattern: '^[a-z0-9_-]+$', message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`, @@ -51,60 +51,50 @@ export const CustomFieldConfigurationWithoutTypeRt = rt.strict({ /** * custom field options - required */ - required: rt.boolean, + required: z.boolean(), }); -export const TextCustomFieldConfigurationRt = rt.intersection([ - rt.strict({ type: CustomFieldTextTypeRt }), - CustomFieldConfigurationWithoutTypeRt, - rt.exact( - rt.partial({ - defaultValue: rt.union([CaseCustomFieldTextWithValidationValueRt('defaultValue'), rt.null]), - }) - ), -]); +export const TextCustomFieldConfigurationSchema = CustomFieldConfigurationWithoutTypeSchema.extend({ + type: CustomFieldTextTypeSchema, + defaultValue: CaseCustomFieldTextWithValidationValueSchema('defaultValue').nullable().optional(), +}); -export const ToggleCustomFieldConfigurationRt = rt.intersection([ - rt.strict({ type: CustomFieldToggleTypeRt }), - CustomFieldConfigurationWithoutTypeRt, - rt.exact( - rt.partial({ - defaultValue: rt.union([rt.boolean, rt.null]), - }) - ), -]); +export const ToggleCustomFieldConfigurationSchema = + CustomFieldConfigurationWithoutTypeSchema.extend({ + type: CustomFieldToggleTypeSchema, + defaultValue: z.boolean().nullable().optional(), + }); -export const NumberCustomFieldConfigurationRt = rt.intersection([ - rt.strict({ type: CustomFieldNumberTypeRt }), - CustomFieldConfigurationWithoutTypeRt, - rt.exact( - rt.partial({ - defaultValue: rt.union([ - CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'defaultValue' }), - rt.null, - ]), - }) - ), -]); +export const NumberCustomFieldConfigurationSchema = + CustomFieldConfigurationWithoutTypeSchema.extend({ + type: CustomFieldNumberTypeSchema, + defaultValue: CaseCustomFieldNumberWithValidationValueSchema({ fieldName: 'defaultValue' }) + .nullable() + .optional(), + }); -export const CustomFieldsConfigurationRt = limitedArraySchema({ - codec: rt.union([ - TextCustomFieldConfigurationRt, - ToggleCustomFieldConfigurationRt, - NumberCustomFieldConfigurationRt, +export const CustomFieldsConfigurationSchema = limitedArraySchema({ + codec: z.union([ + TextCustomFieldConfigurationSchema, + ToggleCustomFieldConfigurationSchema, + NumberCustomFieldConfigurationSchema, ]), min: 0, max: MAX_CUSTOM_FIELDS_PER_CASE, fieldName: 'customFields', }); -export const ObservableTypesConfigurationRt = limitedArraySchema({ +export const ObservableTypesConfigurationSchema = limitedArraySchema({ min: 0, max: MAX_CUSTOM_OBSERVABLE_TYPES, fieldName: 'observableTypes', - codec: rt.strict({ - key: regexStringRt({ - codec: limitedStringSchema({ fieldName: 'key', min: 1, max: MAX_OBSERVABLE_TYPE_KEY_LENGTH }), + codec: z.object({ + key: regexStringSchema({ + codec: limitedStringSchema({ + fieldName: 'key', + min: 1, + max: MAX_OBSERVABLE_TYPE_KEY_LENGTH, + }), pattern: '^[a-z0-9_-]+$', message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`, }), @@ -116,113 +106,95 @@ export const ObservableTypesConfigurationRt = limitedArraySchema({ }), }); -export const TemplateConfigurationRt = rt.intersection([ - rt.strict({ - /** - * key of template - */ - key: regexStringRt({ - codec: limitedStringSchema({ fieldName: 'key', min: 1, max: MAX_TEMPLATE_KEY_LENGTH }), - pattern: '^[a-z0-9_-]+$', - message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`, - }), - /** - * name of template - */ - name: limitedStringSchema({ fieldName: 'name', min: 1, max: MAX_TEMPLATE_NAME_LENGTH }), - /** - * case fields - */ - caseFields: rt.union([rt.null, CaseBaseOptionalFieldsRequestRt]), +export const TemplateConfigurationSchema = z.object({ + /** + * key of template + */ + key: regexStringSchema({ + codec: limitedStringSchema({ fieldName: 'key', min: 1, max: MAX_TEMPLATE_KEY_LENGTH }), + pattern: '^[a-z0-9_-]+$', + message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`, }), - rt.exact( - rt.partial({ - /** - * description of templates - */ - description: limitedStringSchema({ - fieldName: 'description', - min: 0, - max: MAX_TEMPLATE_DESCRIPTION_LENGTH, - }), - /** - * tags of templates - */ - tags: limitedArraySchema({ - codec: limitedStringSchema({ - fieldName: `template's tag`, - min: 1, - max: MAX_TEMPLATE_TAG_LENGTH, - }), - min: 0, - max: MAX_TAGS_PER_TEMPLATE, - fieldName: `template's tags`, - }), - }) - ), -]); + /** + * name of template + */ + name: limitedStringSchema({ fieldName: 'name', min: 1, max: MAX_TEMPLATE_NAME_LENGTH }), + /** + * case fields + */ + caseFields: CaseBaseOptionalFieldsRequestSchema.nullable(), + /** + * description of templates + */ + description: limitedStringSchema({ + fieldName: 'description', + min: 0, + max: MAX_TEMPLATE_DESCRIPTION_LENGTH, + }).optional(), + /** + * tags of templates + */ + tags: limitedArraySchema({ + codec: limitedStringSchema({ + fieldName: `template's tag`, + min: 1, + max: MAX_TEMPLATE_TAG_LENGTH, + }), + min: 0, + max: MAX_TAGS_PER_TEMPLATE, + fieldName: `template's tags`, + }).optional(), +}); -export const TemplatesConfigurationRt = limitedArraySchema({ - codec: TemplateConfigurationRt, +export const TemplatesConfigurationSchema = limitedArraySchema({ + codec: TemplateConfigurationSchema, min: 0, max: MAX_TEMPLATES_LENGTH, fieldName: 'templates', }); -export const ConfigurationRequestRt = rt.intersection([ - rt.strict({ - /** - * The external connector - */ - connector: CaseConnectorRt, - /** - * Whether to close the case after it has been synced with the external system - */ - closure_type: ClosureTypeRt, - /** - * The plugin owner that manages this configuration - */ - owner: rt.string, - }), - rt.exact( - rt.partial({ - customFields: CustomFieldsConfigurationRt, - templates: TemplatesConfigurationRt, - observableTypes: ObservableTypesConfigurationRt, - }) - ), -]); +export const ConfigurationRequestSchema = z.object({ + /** + * The external connector + */ + connector: CaseConnectorSchema, + /** + * Whether to close the case after it has been synced with the external system + */ + closure_type: ClosureTypeSchema, + /** + * The plugin owner that manages this configuration + */ + owner: z.string(), + customFields: CustomFieldsConfigurationSchema.optional(), + templates: TemplatesConfigurationSchema.optional(), + observableTypes: ObservableTypesConfigurationSchema.optional(), +}); -export const GetConfigurationFindRequestRt = rt.exact( - rt.partial({ - /** - * The configuration plugin owner to filter the search by. If this is left empty the results will include all configurations - * that the user has permissions to access - */ - owner: rt.union([rt.array(rt.string), rt.string]), - }) -); +export const GetConfigurationFindRequestSchema = z.object({ + /** + * The configuration plugin owner to filter the search by. If this is left empty the results will include all configurations + * that the user has permissions to access + */ + owner: z.union([z.array(z.string()), z.string()]).optional(), +}); -export const CaseConfigureRequestParamsRt = rt.strict({ - configuration_id: rt.string, +export const CaseConfigureRequestParamsSchema = z.object({ + configuration_id: z.string(), }); -export const ConfigurationPatchRequestRt = rt.intersection([ - rt.exact( - rt.partial({ - closure_type: ConfigurationBasicWithoutOwnerRt.type.props.closure_type, - connector: ConfigurationBasicWithoutOwnerRt.type.props.connector, - customFields: CustomFieldsConfigurationRt, - templates: TemplatesConfigurationRt, - observableTypes: ObservableTypesConfigurationRt, - }) - ), - rt.strict({ version: rt.string }), -]); +export const ConfigurationPatchRequestSchema = z.object({ + closure_type: ClosureTypeSchema.optional(), + connector: ConfigurationBasicWithoutOwnerSchema.shape.connector.optional(), + customFields: CustomFieldsConfigurationSchema.optional(), + templates: TemplatesConfigurationSchema.optional(), + observableTypes: ObservableTypesConfigurationSchema.optional(), + version: z.string(), +}); -export type ConfigurationRequest = rt.TypeOf; -export type ConfigurationPatchRequest = rt.TypeOf; -export type GetConfigurationFindRequest = rt.TypeOf; +export type ConfigurationRequest = z.infer; +export type ConfigurationPatchRequest = z.infer; +export type GetConfigurationFindRequest = z.infer; export type GetConfigureResponse = Configurations; export type CreateConfigureResponse = Configuration; export type UpdateConfigureResponse = Configuration; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.test.ts index 8ca1a40ff7f16..443c721d17d82 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.test.ts @@ -5,18 +5,13 @@ * 2.0. */ -import { - ConnectorMappingResponseRt, - FindActionConnectorResponseRt, - GetCaseConnectorsResponseRt, -} from './v1'; import { ConnectorMappingResponseSchema, FindActionConnectorResponseSchema, GetCaseConnectorsResponseSchema, -} from '../../api_zod/connector/v1'; +} from './v1'; -describe('FindActionConnectorResponseRt', () => { +describe('FindActionConnectorResponseSchema', () => { const response = [ { id: 'test', @@ -43,42 +38,19 @@ describe('FindActionConnectorResponseRt', () => { ]; it('has expected attributes in request', () => { - const query = FindActionConnectorResponseRt.decode(response); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: response, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = FindActionConnectorResponseRt.decode([ - { - ...response[0], - foo: 'bar', - }, - ]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [response[0]], - }); - }); - - it('zod: has expected attributes in request', () => { const result = FindActionConnectorResponseSchema.safeParse(response); expect(result.success).toBe(true); expect(result.data).toStrictEqual(response); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = FindActionConnectorResponseSchema.safeParse([{ ...response[0], foo: 'bar' }]); expect(result.success).toBe(true); expect(result.data).toStrictEqual([response[0]]); }); }); -describe('GetCaseConnectorsResponseRt', () => { +describe('GetCaseConnectorsResponseSchema', () => { const externalService = { connector_id: 'servicenow-1', connector_name: 'My SN connector', @@ -111,60 +83,12 @@ describe('GetCaseConnectorsResponseRt', () => { }; it('has expected attributes in request', () => { - const query = GetCaseConnectorsResponseRt.decode(defaultReq); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultReq, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = GetCaseConnectorsResponseRt.decode({ - 'servicenow-1': { ...defaultReq['servicenow-1'], externalService, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultReq, - }); - }); - - it('removes foo:bar attributes from externalService object', () => { - const query = GetCaseConnectorsResponseRt.decode({ - 'servicenow-1': { - ...defaultReq['servicenow-1'], - externalService: { ...externalService, foo: 'bar' }, - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultReq, - }); - }); - - it('removes foo:bar attributes from push object', () => { - const query = GetCaseConnectorsResponseRt.decode({ - 'servicenow-1': { - ...defaultReq['servicenow-1'], - push: { ...defaultReq['servicenow-1'].push, foo: 'bar' }, - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultReq, - }); - }); - - it('zod: has expected attributes in request', () => { const result = GetCaseConnectorsResponseSchema.safeParse(defaultReq); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultReq); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = GetCaseConnectorsResponseSchema.safeParse({ 'servicenow-1': { ...defaultReq['servicenow-1'], foo: 'bar' }, }); @@ -173,7 +97,7 @@ describe('GetCaseConnectorsResponseRt', () => { }); }); -describe('ConnectorMappingResponseRt', () => { +describe('ConnectorMappingResponseSchema', () => { const mappings = [ { action_type: 'overwrite', @@ -187,53 +111,8 @@ describe('ConnectorMappingResponseRt', () => { }, ]; - describe('ConnectorMappingResponseRt', () => { + describe('ConnectorMappingResponseSchema', () => { it('has expected attributes in response', () => { - const query = ConnectorMappingResponseRt.decode({ id: 'test', version: 'test', mappings }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { id: 'test', version: 'test', mappings }, - }); - }); - - it('removes foo:bar attributes from the response', () => { - const query = ConnectorMappingResponseRt.decode({ - id: 'test', - version: 'test', - mappings: [ - { ...mappings[0] }, - { - action_type: 'append', - source: 'description', - target: 'not_mapped', - foo: 'bar', - }, - ], - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { id: 'test', version: 'test', mappings }, - }); - }); - - it('removes foo:bar attributes from the mappings', () => { - const query = ConnectorMappingResponseRt.decode({ - id: 'test', - version: 'test', - mappings, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { id: 'test', version: 'test', mappings }, - }); - }); - - it('zod: has expected attributes in response', () => { const result = ConnectorMappingResponseSchema.safeParse({ id: 'test', version: 'test', @@ -243,7 +122,7 @@ describe('ConnectorMappingResponseRt', () => { expect(result.data).toStrictEqual({ id: 'test', version: 'test', mappings }); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ConnectorMappingResponseSchema.safeParse({ id: 'test', version: 'test', diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.ts index 5ec09c3d5878a..29e0661ece897 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/connector/v1.ts @@ -5,60 +5,48 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { ExternalServiceRt } from '../../domain/external_service/v1'; -import { CaseConnectorRt, ConnectorMappingsRt } from '../../domain/connector/v1'; - -const PushDetailsRt = rt.strict({ - latestUserActionPushDate: rt.string, - oldestUserActionPushDate: rt.string, - externalService: ExternalServiceRt, +import { z } from '@kbn/zod/v4'; +import { ExternalServiceSchema } from '../../domain/external_service/v1'; +import { CaseConnectorSchema, ConnectorMappingsSchema } from '../../domain/connector/v1'; + +const PushDetailsSchema = z.object({ + latestUserActionPushDate: z.string(), + oldestUserActionPushDate: z.string(), + externalService: ExternalServiceSchema, }); -const CaseConnectorPushInfoRt = rt.intersection([ - rt.strict({ - needsToBePushed: rt.boolean, - hasBeenPushed: rt.boolean, - }), - rt.exact( - rt.partial({ - details: PushDetailsRt, - }) - ), -]); +const CaseConnectorPushInfoSchema = z.object({ + needsToBePushed: z.boolean(), + hasBeenPushed: z.boolean(), + details: PushDetailsSchema.optional(), +}); -export const GetCaseConnectorsResponseRt = rt.record( - rt.string, - rt.intersection([ - rt.strict({ - push: CaseConnectorPushInfoRt, - }), - CaseConnectorRt, - ]) +export const GetCaseConnectorsResponseSchema = z.record( + z.string(), + CaseConnectorSchema.and(z.object({ push: CaseConnectorPushInfoSchema })) ); -const ActionConnectorResultRt = rt.intersection([ - rt.strict({ - id: rt.string, - actionTypeId: rt.string, - name: rt.string, - isDeprecated: rt.boolean, - isPreconfigured: rt.boolean, - isSystemAction: rt.boolean, - referencedByCount: rt.number, - isConnectorTypeDeprecated: rt.boolean, - }), - rt.exact(rt.partial({ config: rt.record(rt.string, rt.unknown), isMissingSecrets: rt.boolean })), -]); +const ActionConnectorResultSchema = z.object({ + id: z.string(), + actionTypeId: z.string(), + name: z.string(), + isDeprecated: z.boolean(), + isPreconfigured: z.boolean(), + isSystemAction: z.boolean(), + referencedByCount: z.number(), + isConnectorTypeDeprecated: z.boolean(), + config: z.record(z.string(), z.unknown()).optional(), + isMissingSecrets: z.boolean().optional(), +}); -export const FindActionConnectorResponseRt = rt.array(ActionConnectorResultRt); +export const FindActionConnectorResponseSchema = z.array(ActionConnectorResultSchema); -export const ConnectorMappingResponseRt = rt.strict({ - id: rt.string, - version: rt.string, - mappings: ConnectorMappingsRt, +export const ConnectorMappingResponseSchema = z.object({ + id: z.string(), + version: z.string(), + mappings: ConnectorMappingsSchema, }); -export type ConnectorMappingResponse = rt.TypeOf; -export type GetCaseConnectorsResponse = rt.TypeOf; -export type GetCaseConnectorsPushDetails = rt.TypeOf; +export type ConnectorMappingResponse = z.infer; +export type GetCaseConnectorsResponse = z.infer; +export type GetCaseConnectorsPushDetails = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.test.ts index d9e7f6e06a299..c7921de9b7570 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.test.ts @@ -5,64 +5,29 @@ * 2.0. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants'; -import { - CaseCustomFieldTextWithValidationValueRt, - CustomFieldPutRequestRt, - CaseCustomFieldNumberWithValidationValueRt, -} from './v1'; import { CaseCustomFieldTextWithValidationValueSchema, CustomFieldPutRequestSchema, CaseCustomFieldNumberWithValidationValueSchema, -} from '../../api_zod/custom_field/v1'; +} from './v1'; describe('Custom Fields', () => { - describe('CaseCustomFieldTextWithValidationValueRt', () => { - const customFieldValueType = CaseCustomFieldTextWithValidationValueRt('value'); - - it('decodes strings correctly', () => { - const query = customFieldValueType.decode('foobar'); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: 'foobar', - }); - }); - - it('the value cannot be empty', () => { - expect(PathReporter.report(customFieldValueType.decode(''))[0]).toContain( - 'The value field cannot be an empty string.' - ); - }); - - it(`limits the length to ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { - expect( - PathReporter.report( - customFieldValueType.decode('#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1)) - )[0] - ).toContain( - `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` - ); - }); - }); - - describe('CaseCustomFieldTextWithValidationValueSchema (zod)', () => { + describe('CaseCustomFieldTextWithValidationValueSchema', () => { const customFieldValueType = CaseCustomFieldTextWithValidationValueSchema('value'); - it('zod: decodes strings correctly', () => { + it('decodes strings correctly', () => { const result = customFieldValueType.safeParse('foobar'); expect(result.success).toBe(true); expect(result.data).toBe('foobar'); }); - it('zod: the value cannot be empty', () => { + it('the value cannot be empty', () => { const result = customFieldValueType.safeParse(''); expect(result.success).toBe(false); }); - it(`zod: limits the length to ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { + it(`limits the length to ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { const result = customFieldValueType.safeParse( '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1) ); @@ -70,124 +35,55 @@ describe('Custom Fields', () => { }); }); - describe('CustomFieldPutRequestRt', () => { + describe('CustomFieldPutRequestSchema', () => { const defaultRequest = { caseVersion: 'WzQ3LDFd', value: 'this is a text field value', }; it('has expected attributes in request', () => { - const query = CustomFieldPutRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('has expected attributes of toggle field in request', () => { - const newRequest = { - caseVersion: 'WzQ3LDFd', - value: false, - }; - const query = CustomFieldPutRequestRt.decode(newRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: newRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CustomFieldPutRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it(`throws an error when a text customField is longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { - expect( - PathReporter.report( - CustomFieldPutRequestRt.decode({ - caseVersion: 'WzQ3LDFd', - value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), - }) - ) - ).toContain( - `The length of the value is too long. The maximum length is ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}.` - ); - }); - - it('throws an error when a text customField is empty', () => { - expect( - PathReporter.report( - CustomFieldPutRequestRt.decode({ - caseVersion: 'WzQ3LDFd', - value: '', - }) - ) - ).toContain('The value field cannot be an empty string.'); - }); - - it('zod: has expected attributes in request', () => { const result = CustomFieldPutRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CustomFieldPutRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - }); - describe('CaseCustomFieldNumberWithValidationValueSchema (zod)', () => { - const numberCustomFieldValueType = CaseCustomFieldNumberWithValidationValueSchema({ - fieldName: 'value', - }); - - it('zod: should decode number correctly', () => { - const result = numberCustomFieldValueType.safeParse(123); - expect(result.success).toBe(true); - expect(result.data).toBe(123); + it(`does not accept text customField longer than ${MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH}`, () => { + const result = CustomFieldPutRequestSchema.safeParse({ + caseVersion: 'WzQ3LDFd', + value: '#'.repeat(MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH + 1), + }); + expect(result.success).toBe(false); }); - it('zod: should not be more than Number.MAX_SAFE_INTEGER', () => { - const result = numberCustomFieldValueType.safeParse(Number.MAX_SAFE_INTEGER + 1); + it('does not accept empty text customField', () => { + const result = CustomFieldPutRequestSchema.safeParse({ + caseVersion: 'WzQ3LDFd', + value: '', + }); expect(result.success).toBe(false); }); }); - describe('CaseCustomFieldNumberWithValidationValueRt', () => { - const numberCustomFieldValueType = CaseCustomFieldNumberWithValidationValueRt({ + describe('CaseCustomFieldNumberWithValidationValueSchema', () => { + const numberCustomFieldValueType = CaseCustomFieldNumberWithValidationValueSchema({ fieldName: 'value', }); - it('should decode number correctly', () => { - const query = numberCustomFieldValueType.decode(123); - expect(query).toStrictEqual({ - _tag: 'Right', - right: 123, - }); + it('should decode number correctly', () => { + const result = numberCustomFieldValueType.safeParse(123); + expect(result.success).toBe(true); + expect(result.data).toBe(123); }); it('should not be more than Number.MAX_SAFE_INTEGER', () => { - expect( - PathReporter.report(numberCustomFieldValueType.decode(Number.MAX_SAFE_INTEGER + 1))[0] - ).toContain( - 'The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.' - ); - }); - - it('should not be less than Number.MIN_SAFE_INTEGER', () => { - expect( - PathReporter.report(numberCustomFieldValueType.decode(Number.MIN_SAFE_INTEGER - 1))[0] - ).toContain( - 'The value field should be an integer between -(2^53 - 1) and 2^53 - 1, inclusive.' - ); + const result = numberCustomFieldValueType.safeParse(Number.MAX_SAFE_INTEGER + 1); + expect(result.success).toBe(false); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.ts index c3e618278adbe..5f715ab280fca 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/custom_field/v1.ts @@ -5,34 +5,27 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants'; import { limitedStringSchema, limitedNumberAsIntegerSchema } from '../../../schema'; -export const CaseCustomFieldTextWithValidationValueRt = (fieldName: string) => - limitedStringSchema({ - fieldName, - min: 1, - max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH, - }); +export const CaseCustomFieldTextWithValidationValueSchema = (fieldName: string) => + limitedStringSchema({ fieldName, min: 1, max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH }); -export const CaseCustomFieldNumberWithValidationValueRt = ({ fieldName }: { fieldName: string }) => - limitedNumberAsIntegerSchema({ - fieldName, - }); +export const CaseCustomFieldNumberWithValidationValueSchema = ({ + fieldName, +}: { + fieldName: string; +}) => limitedNumberAsIntegerSchema({ fieldName }); -/** - * Update custom_field - */ - -export const CustomFieldPutRequestRt = rt.strict({ - value: rt.union([ - rt.boolean, - rt.null, - CaseCustomFieldTextWithValidationValueRt('value'), - CaseCustomFieldNumberWithValidationValueRt({ fieldName: 'value' }), +export const CustomFieldPutRequestSchema = z.object({ + value: z.union([ + z.boolean(), + z.null(), + CaseCustomFieldTextWithValidationValueSchema('value'), + CaseCustomFieldNumberWithValidationValueSchema({ fieldName: 'value' }), ]), - caseVersion: rt.string, + caseVersion: z.string(), }); -export type CustomFieldPutRequest = rt.TypeOf; +export type CustomFieldPutRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.test.ts index 9240897af9063..9c705f70024be 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.test.ts @@ -5,20 +5,15 @@ * 2.0. */ -import { DocumentResponseRt } from './v1'; -import { DocumentResponseSchema } from '../../api_zod/document/v1'; +import { DocumentResponseSchema } from './v1'; describe('Documents', () => { - describe('DocumentResponseRt', () => { + describe('DocumentResponseSchema', () => { it('has expected attributes in request', () => { const defaultRequest = [{ id: '1', index: '2', attached_at: '3' }]; - - const query = DocumentResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = DocumentResponseSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('multiple attributes in request', () => { @@ -26,32 +21,12 @@ describe('Documents', () => { { id: '1', index: '2', attached_at: '3' }, { id: '2', index: '3', attached_at: '4' }, ]; - const query = DocumentResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const defaultRequest = [{ id: '1', index: '2', attached_at: '3' }]; - const query = DocumentResponseRt.decode([{ ...defaultRequest[0], foo: 'bar' }]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const defaultRequest = [{ id: '1', index: '2', attached_at: '3' }]; const result = DocumentResponseSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const defaultRequest = [{ id: '1', index: '2', attached_at: '3' }]; const result = DocumentResponseSchema.safeParse([{ ...defaultRequest[0], foo: 'bar' }]); expect(result.success).toBe(true); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.ts index a408e8844993b..c0628a7b82623 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/document/v1.ts @@ -5,13 +5,14 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; -const DocumentRt = rt.strict({ - id: rt.string, - index: rt.string, - attached_at: rt.string, +const DocumentSchema = z.object({ + id: z.string(), + index: z.string(), + attached_at: z.string(), }); -export const DocumentResponseRt = rt.array(DocumentRt); -export type DocumentResponse = rt.TypeOf; +export const DocumentResponseSchema = z.array(DocumentSchema); + +export type DocumentResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.test.ts index cf12cb401ad69..0ec296a6f63ce 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.test.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { ExternalServiceResponseRt } from './v1'; -import { ExternalServiceResponseSchema } from '../../api_zod/external_service/v1'; +import { ExternalServiceResponseSchema } from './v1'; -describe('ExternalServiceResponseRt', () => { +describe('ExternalServiceResponseSchema', () => { const defaultRequest = { title: 'case_title', id: 'basic-case-id', @@ -24,42 +23,12 @@ describe('ExternalServiceResponseRt', () => { }; it('has expected attributes in request', () => { - const query = ExternalServiceResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ExternalServiceResponseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from comments', () => { - const query = ExternalServiceResponseRt.decode({ - ...defaultRequest, - comments: [{ ...defaultRequest.comments[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ExternalServiceResponseSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ExternalServiceResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.ts index a9ff0b12e77fb..2a8cb66689aaa 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/external_service/v1.ts @@ -5,28 +5,22 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; -export const ExternalServiceResponseRt = rt.intersection([ - rt.strict({ - title: rt.string, - id: rt.string, - pushedDate: rt.string, - url: rt.string, - }), - rt.exact( - rt.partial({ - comments: rt.array( - rt.intersection([ - rt.strict({ - commentId: rt.string, - pushedDate: rt.string, - }), - rt.exact(rt.partial({ externalCommentId: rt.string })), - ]) - ), - }) - ), -]); +export const ExternalServiceResponseSchema = z.object({ + title: z.string(), + id: z.string(), + pushedDate: z.string(), + url: z.string(), + comments: z + .array( + z.object({ + commentId: z.string(), + pushedDate: z.string(), + externalCommentId: z.string().optional(), + }) + ) + .optional(), +}); -export type ExternalServiceResponse = rt.TypeOf; +export type ExternalServiceResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.test.ts index 437c169e0a6f9..ad7ab44b80f24 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.test.ts @@ -5,74 +5,41 @@ * 2.0. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { - SingleCaseMetricsRequestRt, - CasesMetricsRequestRt, - SingleCaseMetricsResponseRt, - CasesMetricsResponseRt, - CaseMetricsFeature, -} from './v1'; import { SingleCaseMetricsRequestSchema, CasesMetricsRequestSchema, SingleCaseMetricsResponseSchema, CasesMetricsResponseSchema, -} from '../../api_zod/metrics/v1'; + CaseMetricsFeature, +} from './v1'; describe('Metrics case', () => { - describe('SingleCaseMetricsRequestRt', () => { + describe('SingleCaseMetricsRequestSchema', () => { const defaultRequest = { features: [CaseMetricsFeature.ALERTS_COUNT, CaseMetricsFeature.LIFESPAN], }; it('has expected attributes in request', () => { - const query = SingleCaseMetricsRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = SingleCaseMetricsRequestRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = SingleCaseMetricsRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = SingleCaseMetricsRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - describe('errors', () => { - it('has invalid feature in request', () => { - expect( - PathReporter.report( - SingleCaseMetricsRequestRt.decode({ - features: [CaseMetricsFeature.MTTR], - }) - )[0] - ).toContain('Invalid value "mttr" supplied'); + it('does not accept invalid feature in request', () => { + const result = SingleCaseMetricsRequestSchema.safeParse({ + features: [CaseMetricsFeature.MTTR], }); + expect(result.success).toBe(false); }); }); - describe('CasesMetricsRequestRt', () => { + describe('CasesMetricsRequestSchema', () => { const defaultRequest = { features: [CaseMetricsFeature.MTTR], to: 'now-1d', @@ -81,67 +48,33 @@ describe('Metrics case', () => { }; it('has expected attributes in request', () => { - const query = CasesMetricsRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesMetricsRequestRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from when partial fields', () => { - const query = CasesMetricsRequestRt.decode({ - features: [CaseMetricsFeature.MTTR], - to: 'now-1d', - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - features: [CaseMetricsFeature.MTTR], - to: 'now-1d', - }, - }); - }); - it('zod: has expected attributes in request', () => { const result = CasesMetricsRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CasesMetricsRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - describe('errors', () => { - it('has invalid feature in request', () => { - expect( - PathReporter.report( - CasesMetricsRequestRt.decode({ - features: ['foobar'], - }) - )[0] - ).toContain('Invalid value "foobar" supplied'); + it('strips unknown fields from partial fields', () => { + const partialRequest = { features: [CaseMetricsFeature.MTTR], foo: 'bar' }; + const result = CasesMetricsRequestSchema.safeParse(partialRequest); + expect(result.success).toBe(true); + expect(result.data).not.toHaveProperty('foo'); + }); + + it('does not accept invalid feature in request', () => { + const result = CasesMetricsRequestSchema.safeParse({ + features: ['foobar'], }); + expect(result.success).toBe(false); }); }); - describe('SingleCaseMetricsResponseRt', () => { + describe('SingleCaseMetricsResponseSchema', () => { const defaultRequest = { alerts: { count: 5, @@ -149,8 +82,8 @@ describe('Metrics case', () => { total: 3, values: [ { name: 'first-host', id: 'first-host-id', count: 3 }, - { id: 'second-host-id', count: 2, name: undefined }, - { id: 'third-host-id', count: 3, name: undefined }, + { id: 'second-host-id', count: 2 }, + { id: 'third-host-id', count: 3 }, ], }, users: { @@ -180,221 +113,130 @@ describe('Metrics case', () => { }; it('has expected attributes in request', () => { - const query = SingleCaseMetricsResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = SingleCaseMetricsResponseSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = SingleCaseMetricsResponseRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from alerts', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from alerts', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, alerts: { ...defaultRequest.alerts, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from hosts', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from hosts', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, - alerts: { ...defaultRequest.alerts, hosts: { ...defaultRequest.alerts.hosts, foo: 'bar' } }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + alerts: { + ...defaultRequest.alerts, + hosts: { ...defaultRequest.alerts!.hosts, foo: 'bar' }, + }, }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from users', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from users', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, - alerts: { ...defaultRequest.alerts, users: { ...defaultRequest.alerts.users, foo: 'bar' } }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + alerts: { + ...defaultRequest.alerts, + users: { ...defaultRequest.alerts!.users, foo: 'bar' }, + }, }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from connectors', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from connectors', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, - connectors: { total: 1, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + connectors: { ...defaultRequest.connectors, foo: 'bar' }, }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from actions', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from actions', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, actions: { ...defaultRequest.actions, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from isolate hosts', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from isolate hosts', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, actions: { ...defaultRequest.actions, - isolateHost: { ...defaultRequest.actions.isolateHost, foo: 'bar' }, + isolateHost: { + ...defaultRequest.actions!.isolateHost, + isolate: { ...defaultRequest.actions!.isolateHost!.isolate, foo: 'bar' }, + }, }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from unisolate host', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from unisolate host', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, actions: { ...defaultRequest.actions, isolateHost: { - ...defaultRequest.actions.isolateHost, - unisolate: { foo: 'bar', total: 2 }, + ...defaultRequest.actions!.isolateHost, + unisolate: { ...defaultRequest.actions!.isolateHost!.unisolate, foo: 'bar' }, }, }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from lifespan', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from lifespan', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, lifespan: { ...defaultRequest.lifespan, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from status info', () => { - const query = SingleCaseMetricsResponseRt.decode({ + it('strips unknown fields from status info', () => { + const result = SingleCaseMetricsResponseSchema.safeParse({ ...defaultRequest, lifespan: { ...defaultRequest.lifespan, - statusInfo: { foo: 'bar', ...defaultRequest.lifespan.statusInfo }, + statusInfo: { ...defaultRequest.lifespan!.statusInfo, foo: 'bar' }, }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - // Zod strips keys with undefined values, so omit name: undefined from host values - const zodRequest = { - ...defaultRequest, - alerts: { - ...defaultRequest.alerts, - hosts: { - ...defaultRequest.alerts.hosts, - values: [ - { name: 'first-host', id: 'first-host-id', count: 3 }, - { id: 'second-host-id', count: 2 }, - { id: 'third-host-id', count: 3 }, - ], - }, - }, - }; - const result = SingleCaseMetricsResponseSchema.safeParse(zodRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(zodRequest); - }); - - it('zod: strips unknown fields', () => { - const zodRequest = { - ...defaultRequest, - alerts: { - ...defaultRequest.alerts, - hosts: { - ...defaultRequest.alerts.hosts, - values: [ - { name: 'first-host', id: 'first-host-id', count: 3 }, - { id: 'second-host-id', count: 2 }, - { id: 'third-host-id', count: 3 }, - ], - }, - }, - }; - const result = SingleCaseMetricsResponseSchema.safeParse({ ...zodRequest, foo: 'bar' }); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(zodRequest); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CasesMetricsResponseRt', () => { + describe('CasesMetricsResponseSchema', () => { const defaultRequest = { mttr: 1 }; it('has expected attributes in request', () => { - const query = CasesMetricsResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CasesMetricsResponseRt.decode({ - mttr: null, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - mttr: null, - }, - }); - }); - - it('zod: has expected attributes in request', () => { const result = CasesMetricsResponseSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CasesMetricsResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.ts index 43302cfb3bd80..2b346b05ae2ab 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/metrics/v1.ts @@ -5,15 +5,7 @@ * 2.0. */ -import * as rt from 'io-ts'; - -export type SingleCaseMetricsRequest = rt.TypeOf; -export type SingleCaseMetricsResponse = rt.TypeOf; -export type CasesMetricsRequest = rt.TypeOf; -export type CasesMetricsResponse = rt.TypeOf; -export type AlertHostsMetrics = rt.TypeOf; -export type AlertUsersMetrics = rt.TypeOf; -export type StatusInfo = rt.TypeOf; +import { z } from '@kbn/zod/v4'; export enum CaseMetricsFeature { ALERTS_COUNT = 'alerts.count', @@ -26,198 +18,201 @@ export enum CaseMetricsFeature { STATUS = 'status', } -export const SingleCaseMetricsFeatureFieldRt = rt.union([ - rt.literal(CaseMetricsFeature.ALERTS_COUNT), - rt.literal(CaseMetricsFeature.ALERTS_USERS), - rt.literal(CaseMetricsFeature.ALERTS_HOSTS), - rt.literal(CaseMetricsFeature.ACTIONS_ISOLATE_HOST), - rt.literal(CaseMetricsFeature.CONNECTORS), - rt.literal(CaseMetricsFeature.LIFESPAN), +export const SingleCaseMetricsFeatureFieldSchema = z.union([ + z.literal(CaseMetricsFeature.ALERTS_COUNT), + z.literal(CaseMetricsFeature.ALERTS_USERS), + z.literal(CaseMetricsFeature.ALERTS_HOSTS), + z.literal(CaseMetricsFeature.ACTIONS_ISOLATE_HOST), + z.literal(CaseMetricsFeature.CONNECTORS), + z.literal(CaseMetricsFeature.LIFESPAN), ]); -export const CasesMetricsFeatureFieldRt = rt.union([ - SingleCaseMetricsFeatureFieldRt, - rt.literal(CaseMetricsFeature.MTTR), - rt.literal(CaseMetricsFeature.STATUS), +export const CasesMetricsFeatureFieldSchema = z.union([ + SingleCaseMetricsFeatureFieldSchema, + z.literal(CaseMetricsFeature.MTTR), + z.literal(CaseMetricsFeature.STATUS), ]); -const StatusInfoRt = rt.strict({ +const StatusInfoSchema = z.object({ /** * Duration the case was in the open status in milliseconds */ - openDuration: rt.number, + openDuration: z.number(), /** * Duration the case was in the in-progress status in milliseconds. Zero indicates the case was never in-progress. */ - inProgressDuration: rt.number, + inProgressDuration: z.number(), /** * The ISO string representation of the dates the case was reopened */ - reopenDates: rt.array(rt.string), + reopenDates: z.array(z.string()), }); -const AlertHostsMetricsRt = rt.strict({ +const AlertHostsMetricsSchema = z.object({ /** * Total unique hosts represented in the alerts */ - total: rt.number, - values: rt.array( - rt.strict({ + total: z.number(), + values: z.array( + z.object({ /** * Host name */ - name: rt.union([rt.string, rt.undefined]), + name: z.string().optional(), /** * Unique identifier for the host */ - id: rt.string, + id: z.string(), /** * Number of alerts that have this particular host name */ - count: rt.number, + count: z.number(), }) ), }); -const AlertUsersMetricsRt = rt.strict({ +const AlertUsersMetricsSchema = z.object({ /** * Total unique users represented in the alerts */ - total: rt.number, - values: rt.array( - rt.strict({ + total: z.number(), + values: z.array( + z.object({ /** * Username */ - name: rt.string, + name: z.string(), /** * Number of alerts that have this particular username */ - count: rt.number, + count: z.number(), }) ), }); -export const SingleCaseMetricsRequestRt = rt.strict({ +export const SingleCaseMetricsRequestSchema = z.object({ /** * The metrics to retrieve. */ - features: rt.array(SingleCaseMetricsFeatureFieldRt), + features: z.array(SingleCaseMetricsFeatureFieldSchema), }); -export const CasesMetricsRequestRt = rt.intersection([ - rt.strict({ - /** - * The metrics to retrieve. - */ - features: rt.array(CasesMetricsFeatureFieldRt), - }), - rt.exact( - rt.partial({ +export const CasesMetricsRequestSchema = z.object({ + /** + * The metrics to retrieve. + */ + features: z.array(CasesMetricsFeatureFieldSchema), + /** + * A KQL date. If used all cases created after (gte) the from date will be returned + */ + from: z.string().optional(), + /** + * A KQL date. If used all cases created before (lte) the to date will be returned. + */ + to: z.string().optional(), + /** + * The owner(s) to filter by. The user making the request must have privileges to retrieve cases of that + * ownership or they will be ignored. If no owner is included, then all ownership types will be included in the response + * that the user has access to. + */ + owner: z.union([z.array(z.string()), z.string()]).optional(), +}); + +export const SingleCaseMetricsResponseSchema = z.object({ + alerts: z + .object({ /** - * A KQL date. If used all cases created after (gte) the from date will be returned + * Number of alerts attached to the case */ - from: rt.string, + count: z.number().optional(), /** - * A KQL date. If used all cases created before (lte) the to date will be returned. + * Host information represented from the alerts attached to this case */ - to: rt.string, + hosts: AlertHostsMetricsSchema.optional(), /** - * The owner(s) to filter by. The user making the request must have privileges to retrieve cases of that - * ownership or they will be ignored. If no owner is included, then all ownership types will be included in the response - * that the user has access to. + * User information represented from the alerts attached to this case */ - owner: rt.union([rt.array(rt.string), rt.string]), + users: AlertUsersMetricsSchema.optional(), }) - ), -]); - -export const SingleCaseMetricsResponseRt = rt.exact( - rt.partial({ - alerts: rt.exact( - rt.partial({ - /** - * Number of alerts attached to the case - */ - count: rt.number, - /** - * Host information represented from the alerts attached to this case - */ - hosts: AlertHostsMetricsRt, - /** - * User information represented from the alerts attached to this case - */ - users: AlertUsersMetricsRt, - }) - ), - /** - * External connectors associated with the case - */ - connectors: rt.strict({ + .optional(), + /** + * External connectors associated with the case + */ + connectors: z + .object({ /** * Total number of connectors in the case */ - total: rt.number, - }), - /** - * Actions taken within the case - */ - actions: rt.exact( - rt.partial({ - isolateHost: rt.strict({ + total: z.number(), + }) + .optional(), + /** + * Actions taken within the case + */ + actions: z + .object({ + isolateHost: z + .object({ /** * Isolate host action information */ - isolate: rt.strict({ + isolate: z.object({ /** * Total times the isolate host action has been performed */ - total: rt.number, + total: z.number(), }), /** * Unisolate host action information */ - unisolate: rt.strict({ + unisolate: z.object({ /** * Total times the unisolate host action has been performed */ - total: rt.number, + total: z.number(), }), - }), - }) - ), - /** - * The case's open,close,in-progress details - */ - lifespan: rt.strict({ + }) + .optional(), + }) + .optional(), + /** + * The case's open,close,in-progress details + */ + lifespan: z + .object({ /** * Date the case was created, in ISO format */ - creationDate: rt.string, + creationDate: z.string(), /** * Date the case was closed, in ISO format. Will be null if the case is not currently closed */ - closeDate: rt.union([rt.string, rt.null]), + closeDate: z.string().nullable(), /** * The case's status information regarding durations in a specific status */ - statusInfo: StatusInfoRt, - }), - }) -); + statusInfo: StatusInfoSchema, + }) + .optional(), +}); -export const CasesMetricsResponseRt = rt.exact( - rt.partial({ - /** - * The average resolve time of all cases in seconds - */ - mttr: rt.union([rt.number, rt.null]), - /** - * The number of total cases per status - */ - status: rt.strict({ open: rt.number, inProgress: rt.number, closed: rt.number }), - }) -); +export const CasesMetricsResponseSchema = z.object({ + /** + * The average resolve time of all cases in seconds + */ + mttr: z.number().nullable().optional(), + /** + * The number of total cases per status + */ + status: z.object({ open: z.number(), inProgress: z.number(), closed: z.number() }).optional(), +}); -export type CasesMetricsFeatureField = rt.TypeOf; -export type SingleCaseMetricsFeatureField = rt.TypeOf; +export type SingleCaseMetricsRequest = z.infer; +export type CasesMetricsRequest = z.infer; +export type SingleCaseMetricsResponse = z.infer; +export type CasesMetricsResponse = z.infer; +export type AlertHostsMetrics = z.infer; +export type AlertUsersMetrics = z.infer; +export type StatusInfo = z.infer; +export type SingleCaseMetricsFeatureField = z.infer; +export type CasesMetricsFeatureField = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.test.ts index caf511f42b531..a011f9dde2353 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.test.ts @@ -5,13 +5,9 @@ * 2.0. */ -import { AddObservableRequestRt, UpdateObservableRequestRt } from './v1'; -import { - AddObservableRequestSchema, - UpdateObservableRequestSchema, -} from '../../api_zod/observable/v1'; +import { AddObservableRequestSchema, UpdateObservableRequestSchema } from './v1'; -describe('AddObservableRequestRT', () => { +describe('AddObservableRequestSchema', () => { it('has expected attributes in request', () => { const defaultRequest = { observable: { @@ -20,30 +16,13 @@ describe('AddObservableRequestRT', () => { value: 'email@example.com', }, }; - - const query = AddObservableRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const defaultRequest = { - observable: { - description: null, - typeKey: 'ef528526-2af9-4345-9b78-046512c5bbd6', - value: 'email@example.com', - }, - }; const result = AddObservableRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); -describe('UpdateObservableRequestRT', () => { +describe('UpdateObservableRequestSchema', () => { it('has expected attributes in request', () => { const defaultRequest = { observable: { @@ -51,22 +30,6 @@ describe('UpdateObservableRequestRT', () => { value: 'email@example.com', }, }; - - const query = UpdateObservableRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const defaultRequest = { - observable: { - description: null, - value: 'email@example.com', - }, - }; const result = UpdateObservableRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.ts index 62ce28ca3dc35..2ced34e9d068e 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/observable/v1.ts @@ -5,35 +5,31 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { CaseObservableBaseRt } from '../../domain/observable/v1'; +import { z } from '@kbn/zod/v4'; +import { CaseObservableBaseSchema } from '../../domain/observable/v1'; -/** - * Observables - */ -export const ObservablePostRt = CaseObservableBaseRt; +export const ObservablePostSchema = CaseObservableBaseSchema; -export const ObservablePatchRt = rt.strict({ - value: rt.string, - description: rt.union([rt.string, rt.null]), +export const ObservablePatchSchema = z.object({ + value: z.string(), + description: z.string().nullable(), }); -export type ObservablePatch = rt.TypeOf; -export type ObservablePost = rt.TypeOf; - -export const AddObservableRequestRt = rt.strict({ - observable: ObservablePostRt, +export const AddObservableRequestSchema = z.object({ + observable: ObservablePostSchema, }); -export const UpdateObservableRequestRt = rt.strict({ - observable: ObservablePatchRt, +export const UpdateObservableRequestSchema = z.object({ + observable: ObservablePatchSchema, }); -export const BulkAddObservablesRequestRt = rt.strict({ - caseId: rt.string, - observables: rt.array(ObservablePostRt), +export const BulkAddObservablesRequestSchema = z.object({ + caseId: z.string(), + observables: z.array(ObservablePostSchema), }); -export type AddObservableRequest = rt.TypeOf; -export type UpdateObservableRequest = rt.TypeOf; -export type BulkAddObservablesRequest = rt.TypeOf; +export type ObservablePost = z.infer; +export type ObservablePatch = z.infer; +export type AddObservableRequest = z.infer; +export type UpdateObservableRequest = z.infer; +export type BulkAddObservablesRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/stats/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/stats/v1.ts index 002563258d889..847c52e030bb7 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/stats/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/stats/v1.ts @@ -5,31 +5,29 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; -export const CasesStatusResponseRt = rt.strict({ - count_open_cases: rt.number, - count_in_progress_cases: rt.number, - count_closed_cases: rt.number, +export const CasesStatusResponseSchema = z.object({ + count_open_cases: z.number(), + count_in_progress_cases: z.number(), + count_closed_cases: z.number(), }); -export const CasesStatusRequestRt = rt.exact( - rt.partial({ - /** - * A KQL date. If used all cases created after (gte) the from date will be returned - */ - from: rt.string, - /** - * A KQL date. If used all cases created before (lte) the to date will be returned. - */ - to: rt.string, - /** - * The owner of the cases to retrieve the status stats from. If no owner is provided the stats for all cases - * that the user has access to will be returned. - */ - owner: rt.union([rt.array(rt.string), rt.string]), - }) -); +export const CasesStatusRequestSchema = z.object({ + /** + * A KQL date. If used all cases created after (gte) the from date will be returned + */ + from: z.string().optional(), + /** + * A KQL date. If used all cases created before (lte) the to date will be returned. + */ + to: z.string().optional(), + /** + * The owner of the cases to retrieve the status stats from. If no owner is provided the stats for all cases + * that the user has access to will be returned. + */ + owner: z.union([z.array(z.string()), z.string()]).optional(), +}); -export type CasesStatusResponse = rt.TypeOf; -export type CasesStatusRequest = rt.TypeOf; +export type CasesStatusResponse = z.infer; +export type CasesStatusRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.test.ts index 09159b2e23a76..26200ebef0ed8 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.test.ts @@ -6,15 +6,10 @@ */ import { MAX_SUGGESTED_PROFILES } from '../../../constants'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { GetCaseUsersResponseRt, SuggestUserProfilesRequestRt } from './v1'; -import { - GetCaseUsersResponseSchema, - SuggestUserProfilesRequestSchema, -} from '../../api_zod/user/v1'; +import { GetCaseUsersResponseSchema, SuggestUserProfilesRequestSchema } from './v1'; describe('User', () => { - describe('GetCaseUsersResponseRt', () => { + describe('GetCaseUsersResponseSchema', () => { const defaultRequest = { assignees: [ { @@ -78,84 +73,12 @@ describe('User', () => { }; it('has expected attributes in request', () => { - const query = GetCaseUsersResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = GetCaseUsersResponseRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from assigned users', () => { - const query = GetCaseUsersResponseRt.decode({ - ...defaultRequest, - assignees: [{ ...defaultRequest.assignees[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, assignees: [{ ...defaultRequest.assignees[0] }] }, - }); - }); - - it('removes foo:bar attributes from unassigned users', () => { - const query = GetCaseUsersResponseRt.decode({ - ...defaultRequest, - unassignedUsers: [{ ...defaultRequest.unassignedUsers[1], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, unassignedUsers: [{ ...defaultRequest.unassignedUsers[1] }] }, - }); - }); - - it('removes foo:bar attributes from participants', () => { - const query = GetCaseUsersResponseRt.decode({ - ...defaultRequest, - participants: [{ ...defaultRequest.participants[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, participants: [{ ...defaultRequest.participants[0] }] }, - }); - }); - - it('removes foo:bar attributes from reporter', () => { - const query = GetCaseUsersResponseRt.decode({ - ...defaultRequest, - reporter: { - ...defaultRequest.reporter, - foo: 'bar', - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = GetCaseUsersResponseSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = GetCaseUsersResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); @@ -163,7 +86,7 @@ describe('User', () => { }); describe('UserProfile', () => { - describe('SuggestUserProfilesRequestRt', () => { + describe('SuggestUserProfilesRequestSchema', () => { const defaultRequest = { name: 'damaged_raccoon', owners: ['cases'], @@ -171,93 +94,34 @@ describe('User', () => { }; it('has expected attributes in request', () => { - const query = SuggestUserProfilesRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - name: 'damaged_raccoon', - owners: ['cases'], - size: 5, - }, - }); + const result = SuggestUserProfilesRequestSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('has only name and owner in request', () => { - const query = SuggestUserProfilesRequestRt.decode({ - name: 'damaged_raccoon', - owners: ['cases'], + it('strips unknown fields', () => { + const result = SuggestUserProfilesRequestSchema.safeParse({ + ...defaultRequest, foo: 'bar', }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - name: 'damaged_raccoon', - owners: ['cases'], - }, - }); - }); - - it('missing size parameter works correctly', () => { - const query = SuggestUserProfilesRequestRt.decode({ - name: 'di maria', - owners: ['benfica'], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - name: 'di maria', - owners: ['benfica'], - }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = SuggestUserProfilesRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - name: 'damaged_raccoon', - owners: ['cases'], - size: 5, - }, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it(`does not accept size param bigger than ${MAX_SUGGESTED_PROFILES}`, () => { - const query = SuggestUserProfilesRequestRt.decode({ + const result = SuggestUserProfilesRequestSchema.safeParse({ ...defaultRequest, size: MAX_SUGGESTED_PROFILES + 1, }); - - expect(PathReporter.report(query)).toContain('The size field cannot be more than 10.'); + expect(result.success).toBe(false); }); it('does not accept size param lower than 1', () => { - const query = SuggestUserProfilesRequestRt.decode({ - ...defaultRequest, - size: 0, - }); - - expect(PathReporter.report(query)).toContain('The size field cannot be less than 1.'); - }); - - it('zod: has expected attributes in request', () => { - const result = SuggestUserProfilesRequestSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { const result = SuggestUserProfilesRequestSchema.safeParse({ ...defaultRequest, - foo: 'bar', + size: 0, }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + expect(result.success).toBe(false); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.ts index 488bcbd91c5e3..f4f913c779e56 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/user/v1.ts @@ -5,33 +5,23 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { MAX_SUGGESTED_PROFILES } from '../../../constants'; import { limitedNumberSchema } from '../../../schema'; -import { UserWithProfileInfoRt } from '../../domain/user/v1'; +import { UserWithProfileInfoSchema } from '../../domain/user/v1'; -export const GetCaseUsersResponseRt = rt.strict({ - assignees: rt.array(UserWithProfileInfoRt), - unassignedUsers: rt.array(UserWithProfileInfoRt), - participants: rt.array(UserWithProfileInfoRt), - reporter: UserWithProfileInfoRt, +export const GetCaseUsersResponseSchema = z.object({ + assignees: z.array(UserWithProfileInfoSchema), + unassignedUsers: z.array(UserWithProfileInfoSchema), + participants: z.array(UserWithProfileInfoSchema), + reporter: UserWithProfileInfoSchema, }); -export type GetCaseUsersResponse = rt.TypeOf; - -/** - * User Profiles - */ -export const SuggestUserProfilesRequestRt = rt.intersection([ - rt.strict({ - name: rt.string, - owners: rt.array(rt.string), - }), - rt.exact( - rt.partial({ - size: limitedNumberSchema({ fieldName: 'size', min: 1, max: MAX_SUGGESTED_PROFILES }), - }) - ), -]); +export const SuggestUserProfilesRequestSchema = z.object({ + name: z.string(), + owners: z.array(z.string()), + size: limitedNumberSchema({ fieldName: 'size', min: 1, max: MAX_SUGGESTED_PROFILES }).optional(), +}); -export type SuggestUserProfilesRequest = rt.TypeOf; +export type GetCaseUsersResponse = z.infer; +export type SuggestUserProfilesRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.test.ts index d61b2d18d3302..3becb7d234562 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.test.ts @@ -9,20 +9,14 @@ import { AttachmentType } from '../../domain/attachment/v1'; import { UserActionTypes } from '../../domain/user_action/action/v1'; import { type CaseUserActionStatsResponse, - CaseUserActionStatsResponseRt, - CaseUserActionStatsRt, - UserActionFindRequestRt, - UserActionFindResponseRt, -} from './v1'; -import { CaseUserActionStatsSchema, UserActionFindRequestSchema, UserActionFindResponseSchema, -} from '../../api_zod/user_action/v1'; +} from './v1'; describe('User actions APIs', () => { describe('Find API', () => { - describe('UserActionFindRequestRt', () => { + describe('UserActionFindRequestSchema', () => { const defaultRequest = { types: [UserActionTypes.comment], sortOrder: 'desc', @@ -31,45 +25,19 @@ describe('User actions APIs', () => { }; it('has expected attributes in request', () => { - const query = UserActionFindRequestRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - page: 1, - perPage: 10, - }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = UserActionFindRequestRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - page: 1, - perPage: 10, - }, - }); - }); - - it('zod: has expected attributes in request', () => { const result = UserActionFindRequestSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual({ ...defaultRequest, page: 1, perPage: 10 }); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = UserActionFindRequestSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual({ ...defaultRequest, page: 1, perPage: 10 }); }); }); - describe('UserActionFindResponseRt', () => { + describe('UserActionFindResponseSchema', () => { const defaultRequest = { userActions: [ { @@ -100,42 +68,12 @@ describe('User actions APIs', () => { }; it('has expected attributes in request', () => { - const query = UserActionFindResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = UserActionFindResponseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from userActions', () => { - const query = UserActionFindResponseRt.decode({ - ...defaultRequest, - userActions: [{ ...defaultRequest.userActions[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = UserActionFindResponseSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = UserActionFindResponseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); @@ -144,38 +82,7 @@ describe('User actions APIs', () => { }); describe('User actions stats API', () => { - describe('CaseUserActionStatsResponseRt', () => { - const defaultRequest: CaseUserActionStatsResponse = { - total: 15, - total_deletions: 0, - total_comments: 10, - total_comment_deletions: 0, - total_comment_creations: 0, - total_hidden_comment_updates: 0, - total_other_actions: 5, - total_other_action_deletions: 0, - }; - - it('has expected attributes in request', () => { - const query = CaseUserActionStatsResponseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CaseUserActionStatsResponseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - }); - - describe('CaseUserActionStatsRt', () => { + describe('CaseUserActionStatsSchema', () => { const defaultRequest: CaseUserActionStatsResponse = { total: 100, total_deletions: 0, @@ -188,30 +95,12 @@ describe('User actions APIs', () => { }; it('has expected attributes in request', () => { - const query = CaseUserActionStatsRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CaseUserActionStatsRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = CaseUserActionStatsSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CaseUserActionStatsSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); diff --git a/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts index 351047ccb4dff..611158785999d 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/api/user_action/v1.ts @@ -5,57 +5,18 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { paginationSchema } from '../../../schema'; +import { z } from '@kbn/zod/v4'; import { MAX_USER_ACTIONS_PER_PAGE } from '../../../constants'; -import { UserActionTypes } from '../../domain/user_action/action/v1'; -import type { CaseUserActionInjectedIdsRt } from '../../domain/user_action/v1'; +import { paginationSchema } from '../../../schema'; +import type { CaseUserActionInjectedIdsSchema } from '../../domain/user_action/v1'; import { - CaseUserActionInjectedDeprecatedIdsRt, - CaseUserActionBasicRt, - UserActionsRt, + CaseUserActionBasicSchema, + CaseUserActionInjectedDeprecatedIdsSchema, + UserActionsSchema, } from '../../domain/user_action/v1'; +import { UserActionTypes } from '../../domain/user_action/action/v1'; import type { AttachmentsV2 } from '../../domain'; -export type UserActionWithResponse = T & { id: string; version: string } & rt.TypeOf< - typeof CaseUserActionInjectedIdsRt - >; - -/** - * User actions stats API - */ -export const CaseUserActionStatsRt = rt.strict({ - total: rt.number, - total_deletions: rt.number, - total_comments: rt.number, - total_comment_deletions: rt.number, - total_comment_creations: rt.number, - total_hidden_comment_updates: rt.number, - total_other_actions: rt.number, - total_other_action_deletions: rt.number, -}); - -export type CaseUserActionStatsResponse = rt.TypeOf; -export const CaseUserActionStatsResponseRt = CaseUserActionStatsRt; - -/** - * Deprecated APIs - */ -export const CaseUserActionDeprecatedResponseRt = rt.intersection([ - CaseUserActionBasicRt, - CaseUserActionInjectedDeprecatedIdsRt, -]); -export const CaseUserActionsDeprecatedResponseRt = rt.array(CaseUserActionDeprecatedResponseRt); -export type CaseUserActionsDeprecatedResponse = rt.TypeOf< - typeof CaseUserActionsDeprecatedResponseRt ->; - -export type CaseUserActionDeprecatedResponse = rt.TypeOf; - -/** - * Find User Actions API - */ - const UserActionAdditionalFindRequestFilterTypes = { action: 'action', alert: 'alert', @@ -68,29 +29,70 @@ const UserActionFindRequestTypes = { ...UserActionAdditionalFindRequestFilterTypes, } as const; -const UserActionFindRequestTypesRt = rt.keyof(UserActionFindRequestTypes); -export type UserActionFindRequestTypes = rt.TypeOf; +type UserActionFindRequestTypeValue = + (typeof UserActionFindRequestTypes)[keyof typeof UserActionFindRequestTypes]; +const UserActionFindRequestTypesValues = Object.values(UserActionFindRequestTypes) as [ + UserActionFindRequestTypeValue, + ...UserActionFindRequestTypeValue[] +]; -export const UserActionFindRequestRt = rt.intersection([ - rt.exact( - rt.partial({ - types: rt.array(UserActionFindRequestTypesRt), - sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - }) - ), - paginationSchema({ maxPerPage: MAX_USER_ACTIONS_PER_PAGE }), -]); +/** + * User actions stats API + */ +export const CaseUserActionStatsSchema = z.object({ + total: z.number(), + total_deletions: z.number(), + total_comments: z.number(), + total_comment_deletions: z.number(), + total_comment_creations: z.number(), + total_hidden_comment_updates: z.number(), + total_other_actions: z.number(), + total_other_action_deletions: z.number(), +}); -export type UserActionFindRequest = rt.TypeOf; +export const CaseUserActionStatsResponseSchema = CaseUserActionStatsSchema; -export const UserActionFindResponseRt = rt.strict({ - userActions: UserActionsRt, - page: rt.number, - perPage: rt.number, - total: rt.number, +/** + * Deprecated APIs + */ +export const CaseUserActionDeprecatedResponseSchema = CaseUserActionBasicSchema.and( + CaseUserActionInjectedDeprecatedIdsSchema +); +export const CaseUserActionsDeprecatedResponseSchema = z.array( + CaseUserActionDeprecatedResponseSchema +); + +/** + * Find User Actions API + */ +export const UserActionFindRequestSchema = paginationSchema({ + maxPerPage: MAX_USER_ACTIONS_PER_PAGE, +}).extend({ + types: z.array(z.enum(UserActionFindRequestTypesValues)).optional(), + sortOrder: z.enum(['desc', 'asc']).optional(), }); -export type UserActionFindResponse = rt.TypeOf; +export const UserActionFindResponseSchema = z.object({ + userActions: UserActionsSchema, + page: z.number(), + perPage: z.number(), + total: z.number(), +}); + +export type CaseUserActionStats = z.infer; +export type CaseUserActionStatsResponse = z.infer; +export type UserActionFindRequest = z.infer; +export type UserActionFindResponse = z.infer; +export type CaseUserActionDeprecatedResponse = z.infer< + typeof CaseUserActionDeprecatedResponseSchema +>; +export type CaseUserActionsDeprecatedResponse = z.infer< + typeof CaseUserActionsDeprecatedResponseSchema +>; +export type UserActionFindRequestTypes = (typeof UserActionFindRequestTypesValues)[number]; +export type UserActionWithResponse = T & { id: string; version: string } & z.infer< + typeof CaseUserActionInjectedIdsSchema + >; export interface UserActionInternalFindResponse extends UserActionFindResponse { latestAttachments: AttachmentsV2; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/attachment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/attachment/v1.ts deleted file mode 100644 index 515bbfa7f4ed3..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/attachment/v1.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { - MAX_BULK_CREATE_ATTACHMENTS, - MAX_BULK_GET_ATTACHMENTS, - MAX_COMMENTS_PER_PAGE, - MAX_COMMENT_LENGTH, - MAX_DELETE_FILES, - MAX_FILENAME_LENGTH, -} from '../../../constants'; -import { - limitedArraySchema, - limitedStringSchema, - NonEmptyString, - paginationSchema, -} from '../../../schema_zod'; -import { - UserCommentAttachmentPayloadSchema, - AlertAttachmentPayloadSchema, - ActionsAttachmentPayloadSchema, - ExternalReferenceNoSOAttachmentPayloadSchema, - ExternalReferenceSOAttachmentPayloadSchema, - ExternalReferenceSOWithoutRefsAttachmentPayloadSchema, - PersistableStateAttachmentPayloadSchema, - AttachmentType, - AttachmentsSchema, - EventAttachmentPayloadSchema, -} from '../../domain_zod/attachment/v1'; - -export { AttachmentType }; - -/** - * Files - */ - -const MIN_DELETE_IDS = 1; - -export const BulkDeleteFileAttachmentsRequestSchema = z.object({ - ids: limitedArraySchema({ - codec: NonEmptyString, - min: MIN_DELETE_IDS, - max: MAX_DELETE_FILES, - fieldName: 'ids', - }), -}); - -export const PostFileAttachmentRequestSchema = z.object({ - file: z.unknown(), - filename: limitedStringSchema({ - fieldName: 'filename', - min: 1, - max: MAX_FILENAME_LENGTH, - }).optional(), -}); - -/** - * Attachments - */ - -export const AttachmentRequestSchema = z.union([ - z.object({ - comment: limitedStringSchema({ fieldName: 'comment', min: 1, max: MAX_COMMENT_LENGTH }), - type: z.literal(AttachmentType.user), - owner: z.string(), - }), - EventAttachmentPayloadSchema, - AlertAttachmentPayloadSchema, - z.object({ - type: z.literal(AttachmentType.actions), - comment: limitedStringSchema({ fieldName: 'comment', min: 1, max: MAX_COMMENT_LENGTH }), - actions: z.object({ - targets: z.array( - z.object({ - hostname: z.string(), - endpointId: z.string(), - }) - ), - type: z.string(), - }), - owner: z.string(), - }), - ExternalReferenceNoSOAttachmentPayloadSchema, - ExternalReferenceSOAttachmentPayloadSchema, - PersistableStateAttachmentPayloadSchema, -]); - -export const AttachmentRequestWithoutRefsSchema = z.union([ - UserCommentAttachmentPayloadSchema, - AlertAttachmentPayloadSchema, - EventAttachmentPayloadSchema, - ActionsAttachmentPayloadSchema, - ExternalReferenceNoSOAttachmentPayloadSchema, - ExternalReferenceSOWithoutRefsAttachmentPayloadSchema, - PersistableStateAttachmentPayloadSchema, -]); - -export const AttachmentPatchRequestSchema = AttachmentRequestSchema.and( - z.object({ id: z.string(), version: z.string() }) -); - -export const AttachmentsFindResponseSchema = z.object({ - comments: AttachmentsSchema, - page: z.number(), - per_page: z.number(), - total: z.number(), -}); - -export const FindAttachmentsQueryParamsSchema = paginationSchema({ - maxPerPage: MAX_COMMENTS_PER_PAGE, -}).extend({ - sortOrder: z.enum(['desc', 'asc']).optional(), -}); - -export const BulkCreateAttachmentsRequestSchema = limitedArraySchema({ - codec: AttachmentRequestSchema, - min: 0, - max: MAX_BULK_CREATE_ATTACHMENTS, - fieldName: 'attachments', -}); - -export const BulkGetAttachmentsRequestSchema = z.object({ - ids: limitedArraySchema({ - codec: z.string(), - min: 1, - max: MAX_BULK_GET_ATTACHMENTS, - fieldName: 'ids', - }), -}); - -export const BulkGetAttachmentsResponseSchema = z.object({ - attachments: AttachmentsSchema, - errors: z.array( - z.object({ - error: z.string(), - message: z.string(), - status: z.number().optional(), - savedObjectId: z.string(), - }) - ), -}); - -export type BulkDeleteFileAttachmentsRequest = z.infer< - typeof BulkDeleteFileAttachmentsRequestSchema ->; -export type PostFileAttachmentRequest = z.infer; -export type AttachmentRequest = z.infer; -export type AttachmentPatchRequest = z.infer; -export type AttachmentsFindResponse = z.infer; -export type FindAttachmentsQueryParams = z.infer; -export type BulkCreateAttachmentsRequest = z.infer; -export type BulkGetAttachmentsRequest = z.infer; -export type BulkGetAttachmentsResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/attachment/v2.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/attachment/v2.ts deleted file mode 100644 index 30129a2b8049e..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/attachment/v2.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { MAX_BULK_CREATE_ATTACHMENTS } from '../../../constants'; -import { limitedArraySchema } from '../../../schema_zod'; -import { - AttachmentRequestSchema, - AttachmentRequestWithoutRefsSchema, - AttachmentPatchRequestSchema, -} from './v1'; -import { - UnifiedAttachmentPayloadSchema, - UnifiedReferenceAttachmentPayloadSchema, - UnifiedValueAttachmentPayloadSchema, -} from '../../domain_zod/attachment/v2'; - -export const UnifiedAttachmentPatchRequestSchema = z.union([ - UnifiedReferenceAttachmentPayloadSchema.extend({ id: z.string(), version: z.string() }), - UnifiedValueAttachmentPayloadSchema.extend({ id: z.string(), version: z.string() }), -]); - -export const AttachmentRequestSchemaV2 = z.union([ - AttachmentRequestSchema, - UnifiedAttachmentPayloadSchema, -]); - -export const AttachmentRequestWithoutRefsSchemaV2 = z.union([ - AttachmentRequestWithoutRefsSchema, - UnifiedAttachmentPayloadSchema, -]); - -export const AttachmentPatchRequestSchemaV2 = z.union([ - AttachmentPatchRequestSchema, - UnifiedAttachmentPatchRequestSchema, -]); - -export const BulkCreateAttachmentsRequestSchemaV2 = limitedArraySchema({ - codec: AttachmentRequestSchemaV2, - min: 0, - max: MAX_BULK_CREATE_ATTACHMENTS, - fieldName: 'attachments', -}); - -export type AttachmentRequestV2 = z.infer; -export type AttachmentPatchRequestV2 = z.infer; -export type BulkCreateAttachmentsRequestV2 = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/case/v1.ts deleted file mode 100644 index f439010e84399..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/case/v1.ts +++ /dev/null @@ -1,419 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { - CASE_EXTENDED_FIELDS, - MAX_ASSIGNEES_FILTER_LENGTH, - MAX_ASSIGNEES_PER_CASE, - MAX_BULK_GET_CASES, - MAX_CASES_PER_PAGE, - MAX_CASES_TO_UPDATE, - MAX_CATEGORY_FILTER_LENGTH, - MAX_CATEGORY_LENGTH, - MAX_CUSTOM_FIELDS_PER_CASE, - MAX_DELETE_IDS_LENGTH, - MAX_DESCRIPTION_LENGTH, - MAX_LENGTH_PER_TAG, - MAX_REPORTERS_FILTER_LENGTH, - MAX_TAGS_FILTER_LENGTH, - MAX_TAGS_PER_CASE, - MAX_TITLE_LENGTH, -} from '../../../constants'; -import { - limitedArraySchema, - limitedStringSchema, - NonEmptyString, - paginationSchema, -} from '../../../schema_zod'; -import { - CaseCustomFieldToggleSchema, - CustomFieldTextTypeSchema, - CustomFieldNumberTypeSchema, -} from '../../domain_zod/custom_field/v1'; -import { - CaseSchema, - CasesSchema, - CaseSettingsSchema, - CaseSeveritySchema, - CaseStatusSchema, - CaseTemplateSchema, - RelatedCaseSchema, - SimilarCaseSchema, -} from '../../domain_zod/case/v1'; -import { CaseConnectorSchema } from '../../domain_zod/connector/v1'; -import { CaseUserProfileSchema, UserSchema } from '../../domain_zod/user/v1'; -import { CasesStatusResponseSchema } from '../stats/v1'; -import { - CaseCustomFieldTextWithValidationValueSchema, - CaseCustomFieldNumberWithValidationValueSchema, -} from '../custom_field/v1'; - -const CaseCustomFieldTextWithValidationSchema = z.object({ - key: z.string(), - type: CustomFieldTextTypeSchema, - value: z.union([CaseCustomFieldTextWithValidationValueSchema('value'), z.null()]), -}); - -const CaseCustomFieldNumberWithValidationSchema = z.object({ - key: z.string(), - type: CustomFieldNumberTypeSchema, - value: z.union([ - CaseCustomFieldNumberWithValidationValueSchema({ fieldName: 'value' }), - z.null(), - ]), -}); - -const CustomFieldForRequestSchema = z.union([ - CaseCustomFieldTextWithValidationSchema, - CaseCustomFieldToggleSchema, - CaseCustomFieldNumberWithValidationSchema, -]); - -export const CaseRequestCustomFieldsSchema = limitedArraySchema({ - codec: CustomFieldForRequestSchema, - fieldName: 'customFields', - min: 0, - max: MAX_CUSTOM_FIELDS_PER_CASE, -}); - -export const CaseBaseOptionalFieldsRequestSchema = z.object({ - description: limitedStringSchema({ - fieldName: 'description', - min: 1, - max: MAX_DESCRIPTION_LENGTH, - }).optional(), - tags: limitedArraySchema({ - codec: limitedStringSchema({ fieldName: 'tag', min: 1, max: MAX_LENGTH_PER_TAG }), - min: 0, - max: MAX_TAGS_PER_CASE, - fieldName: 'tags', - }).optional(), - title: limitedStringSchema({ fieldName: 'title', min: 1, max: MAX_TITLE_LENGTH }).optional(), - connector: CaseConnectorSchema.optional(), - severity: CaseSeveritySchema.optional(), - assignees: limitedArraySchema({ - codec: CaseUserProfileSchema, - fieldName: 'assignees', - min: 0, - max: MAX_ASSIGNEES_PER_CASE, - }).optional(), - category: z - .union([ - limitedStringSchema({ fieldName: 'category', min: 1, max: MAX_CATEGORY_LENGTH }), - z.null(), - ]) - .optional(), - customFields: CaseRequestCustomFieldsSchema.optional(), - settings: CaseSettingsSchema.optional(), - template: CaseTemplateSchema.nullable().optional(), - [CASE_EXTENDED_FIELDS]: z.record(z.string(), z.string()).optional(), -}); - -export const CaseRequestFieldsSchema = CaseBaseOptionalFieldsRequestSchema.extend({ - status: CaseStatusSchema.optional(), - owner: z.string().optional(), -}); - -/** - * Create case - */ -export const CasePostRequestSchema = z.object({ - description: limitedStringSchema({ - fieldName: 'description', - min: 1, - max: MAX_DESCRIPTION_LENGTH, - }), - tags: limitedArraySchema({ - codec: limitedStringSchema({ fieldName: 'tag', min: 1, max: MAX_LENGTH_PER_TAG }), - fieldName: 'tags', - min: 0, - max: MAX_TAGS_PER_CASE, - }), - title: limitedStringSchema({ fieldName: 'title', min: 1, max: MAX_TITLE_LENGTH }), - connector: CaseConnectorSchema, - settings: CaseSettingsSchema, - owner: z.string(), - assignees: limitedArraySchema({ - codec: CaseUserProfileSchema, - fieldName: 'assignees', - min: 0, - max: MAX_ASSIGNEES_PER_CASE, - }).optional(), - severity: CaseSeveritySchema.optional(), - category: z - .union([ - limitedStringSchema({ fieldName: 'category', min: 1, max: MAX_CATEGORY_LENGTH }), - z.null(), - ]) - .optional(), - customFields: CaseRequestCustomFieldsSchema.optional(), - template: CaseTemplateSchema.nullable().optional(), - [CASE_EXTENDED_FIELDS]: z.record(z.string(), z.string()).optional(), -}); - -/** - * Bulk create cases - */ - -const CaseCreateRequestWithOptionalIdSchema = CasePostRequestSchema.extend({ - id: z.string().optional(), -}); - -export const BulkCreateCasesRequestSchema = z.object({ - cases: z.array(CaseCreateRequestWithOptionalIdSchema), -}); - -export const BulkCreateCasesResponseSchema = z.object({ - cases: z.array(CaseSchema), -}); - -/** - * Find cases - */ - -const CasesFindRequestSearchFieldsValues = ['description', 'title', 'incremental_id.text'] as const; -const CasesFindRequestSortFieldsValues = [ - 'title', - 'category', - 'createdAt', - 'updatedAt', - 'closedAt', - 'status', - 'severity', -] as const; - -export const CasesFindRequestSearchFieldsSchema = z.enum(CasesFindRequestSearchFieldsValues); -export const CasesFindRequestSortFieldsSchema = z.enum(CasesFindRequestSortFieldsValues); - -const CasesFindRequestBaseFieldsSchema = paginationSchema({ - maxPerPage: MAX_CASES_PER_PAGE, -}).extend({ - tags: z - .union([ - limitedArraySchema({ - codec: z.string(), - fieldName: 'tags', - min: 0, - max: MAX_TAGS_FILTER_LENGTH, - }), - z.string(), - ]) - .optional(), - status: z.union([CaseStatusSchema, z.array(CaseStatusSchema)]).optional(), - severity: z.union([CaseSeveritySchema, z.array(CaseSeveritySchema)]).optional(), - assignees: z - .union([ - limitedArraySchema({ - codec: z.string(), - fieldName: 'assignees', - min: 0, - max: MAX_ASSIGNEES_FILTER_LENGTH, - }), - z.string(), - ]) - .optional(), - reporters: z - .union([ - limitedArraySchema({ - codec: z.string(), - fieldName: 'reporters', - min: 0, - max: MAX_REPORTERS_FILTER_LENGTH, - }), - z.string(), - ]) - .optional(), - defaultSearchOperator: z.enum(['AND', 'OR']).optional(), - from: z.string().optional(), - search: z.string().optional(), - sortField: CasesFindRequestSortFieldsSchema.optional(), - sortOrder: z.enum(['desc', 'asc']).optional(), - to: z.string().optional(), - owner: z.union([z.array(z.string()), z.string()]).optional(), - category: z - .union([ - limitedArraySchema({ - codec: z.string(), - fieldName: 'category', - min: 0, - max: MAX_CATEGORY_FILTER_LENGTH, - }), - z.string(), - ]) - .optional(), -}); - -export const CasesFindRequestSchema = CasesFindRequestBaseFieldsSchema.extend({ - searchFields: z - .union([z.array(CasesFindRequestSearchFieldsSchema), CasesFindRequestSearchFieldsSchema]) - .optional(), -}); - -const CasesSearchRequestSearchFieldsValues = [ - 'cases.description', - 'cases.title', - 'cases.incremental_id.text', - 'cases.observables.value', - 'cases.customFields.value', - 'cases-comments.comment', - 'cases-comments.alertId', - 'cases-comments.eventId', -] as const; - -export const CasesSearchRequestSearchFieldsSchema = z.enum(CasesSearchRequestSearchFieldsValues); - -export const CasesSearchRequestSchema = CasesFindRequestBaseFieldsSchema.extend({ - customFields: z - .record(z.string(), z.array(z.union([z.string(), z.boolean(), z.number(), z.null()]))) - .optional(), - searchFields: z - .union([z.array(CasesSearchRequestSearchFieldsSchema), CasesSearchRequestSearchFieldsSchema]) - .optional(), -}); - -export const CasesFindRequestWithCustomFieldsSchema = CasesFindRequestSchema.extend({ - customFields: z - .record(z.string(), z.array(z.union([z.string(), z.boolean(), z.number(), z.null()]))) - .optional(), -}); - -export const CasesFindResponseSchema = CasesStatusResponseSchema.extend({ - cases: z.array(CaseSchema), - page: z.number(), - per_page: z.number(), - total: z.number(), -}); - -export const CasesSimilarResponseSchema = z.object({ - cases: z.array(SimilarCaseSchema), - page: z.number(), - per_page: z.number(), - total: z.number(), -}); - -/** - * Delete cases - */ - -export const CasesDeleteRequestSchema = limitedArraySchema({ - codec: NonEmptyString, - min: 1, - max: MAX_DELETE_IDS_LENGTH, - fieldName: 'ids', -}); - -/** - * Resolve case - */ - -export const CaseResolveResponseSchema = z.object({ - case: CaseSchema, - outcome: z.enum(['exactMatch', 'aliasMatch', 'conflict']), - alias_target_id: z.string().optional(), - alias_purpose: z.enum(['savedObjectConversion', 'savedObjectImport']).optional(), -}); - -/** - * Get cases - */ - -export const CasesBulkGetRequestSchema = z.object({ - ids: limitedArraySchema({ codec: z.string(), min: 1, max: MAX_BULK_GET_CASES, fieldName: 'ids' }), -}); - -export const CasesBulkGetResponseSchema = z.object({ - cases: CasesSchema, - errors: z.array( - z.object({ - error: z.string(), - message: z.string(), - status: z.number().optional(), - caseId: z.string(), - }) - ), -}); - -/** - * Update cases - */ - -export const CasePatchRequestSchema = CaseRequestFieldsSchema.extend({ - id: z.string(), - version: z.string(), -}); - -export const CasesPatchRequestSchema = z.object({ - cases: limitedArraySchema({ - codec: CasePatchRequestSchema, - min: 1, - max: MAX_CASES_TO_UPDATE, - fieldName: 'cases', - }), -}); - -/** - * Push case - */ - -export const CasePushRequestParamsSchema = z.object({ - case_id: z.string(), - connector_id: z.string(), -}); - -/** - * Taxonomies - */ - -export const AllTagsFindRequestSchema = z.object({ - owner: z.union([z.array(z.string()), z.string()]).optional(), -}); - -export const AllCategoriesFindRequestSchema = AllTagsFindRequestSchema; -export const AllReportersFindRequestSchema = AllTagsFindRequestSchema; - -export const GetTagsResponseSchema = z.array(z.string()); -export const GetCategoriesResponseSchema = z.array(z.string()); -export const GetReportersResponseSchema = z.array(UserSchema); - -/** - * Alerts - */ - -export const CasesByAlertIDRequestSchema = z.object({ - owner: z.union([z.array(z.string()), z.string()]).optional(), -}); - -export const GetRelatedCasesByAlertResponseSchema = z.array(RelatedCaseSchema); - -export const SimilarCasesSearchRequestSchema = paginationSchema({ maxPerPage: MAX_CASES_PER_PAGE }); - -export const FindCasesContainingAllDocumentsRequestSchema = z.object({ - documentIds: z.array(z.string()).optional(), - alertIds: z.array(z.string()).optional(), - caseIds: z.array(z.string()), -}); - -export const FindCasesContainingAllAlertsResponseSchema = z.object({ - casesWithAllAttachments: z.array(z.string()), -}); - -export type CasePostRequest = z.infer; -export type CaseResolveResponse = z.infer; -export type CasesDeleteRequest = z.infer; -export type CasesByAlertIDRequest = z.infer; -export type CasesFindRequest = z.infer; -export type CasesFindResponse = z.infer; -export type CasePatchRequest = z.infer; -export type CasesPatchRequest = z.infer; -export type GetTagsResponse = z.infer; -export type GetCategoriesResponse = z.infer; -export type GetReportersResponse = z.infer; -export type CasesBulkGetRequest = z.infer; -export type CasesBulkGetResponse = z.infer; -export type BulkCreateCasesRequest = z.infer; -export type BulkCreateCasesResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/configure/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/configure/v1.ts deleted file mode 100644 index e6167ec418229..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/configure/v1.ts +++ /dev/null @@ -1,162 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { - MAX_CUSTOM_FIELDS_PER_CASE, - MAX_CUSTOM_FIELD_KEY_LENGTH, - MAX_CUSTOM_FIELD_LABEL_LENGTH, - MAX_CUSTOM_OBSERVABLE_TYPES, - MAX_OBSERVABLE_TYPE_KEY_LENGTH, - MAX_OBSERVABLE_TYPE_LABEL_LENGTH, - MAX_TAGS_PER_TEMPLATE, - MAX_TEMPLATES_LENGTH, - MAX_TEMPLATE_DESCRIPTION_LENGTH, - MAX_TEMPLATE_KEY_LENGTH, - MAX_TEMPLATE_NAME_LENGTH, - MAX_TEMPLATE_TAG_LENGTH, -} from '../../../constants'; -import { limitedArraySchema, limitedStringSchema, regexStringSchema } from '../../../schema_zod'; -import { - CustomFieldTextTypeSchema, - CustomFieldToggleTypeSchema, - CustomFieldNumberTypeSchema, -} from '../../domain_zod/custom_field/v1'; -import { - ClosureTypeSchema, - ConfigurationBasicWithoutOwnerSchema, -} from '../../domain_zod/configure/v1'; -import { CaseConnectorSchema } from '../../domain_zod/connector/v1'; -import { CaseBaseOptionalFieldsRequestSchema } from '../case/v1'; -import { - CaseCustomFieldTextWithValidationValueSchema, - CaseCustomFieldNumberWithValidationValueSchema, -} from '../custom_field/v1'; - -export const CustomFieldConfigurationWithoutTypeSchema = z.object({ - key: regexStringSchema({ - codec: limitedStringSchema({ fieldName: 'key', min: 1, max: MAX_CUSTOM_FIELD_KEY_LENGTH }), - pattern: '^[a-z0-9_-]+$', - message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`, - }), - label: limitedStringSchema({ fieldName: 'label', min: 1, max: MAX_CUSTOM_FIELD_LABEL_LENGTH }), - required: z.boolean(), -}); - -export const TextCustomFieldConfigurationSchema = CustomFieldConfigurationWithoutTypeSchema.extend({ - type: CustomFieldTextTypeSchema, - defaultValue: CaseCustomFieldTextWithValidationValueSchema('defaultValue').nullable().optional(), -}); - -export const ToggleCustomFieldConfigurationSchema = - CustomFieldConfigurationWithoutTypeSchema.extend({ - type: CustomFieldToggleTypeSchema, - defaultValue: z.boolean().nullable().optional(), - }); - -export const NumberCustomFieldConfigurationSchema = - CustomFieldConfigurationWithoutTypeSchema.extend({ - type: CustomFieldNumberTypeSchema, - defaultValue: CaseCustomFieldNumberWithValidationValueSchema({ fieldName: 'defaultValue' }) - .nullable() - .optional(), - }); - -export const CustomFieldsConfigurationSchema = limitedArraySchema({ - codec: z.union([ - TextCustomFieldConfigurationSchema, - ToggleCustomFieldConfigurationSchema, - NumberCustomFieldConfigurationSchema, - ]), - min: 0, - max: MAX_CUSTOM_FIELDS_PER_CASE, - fieldName: 'customFields', -}); - -export const ObservableTypesConfigurationSchema = limitedArraySchema({ - min: 0, - max: MAX_CUSTOM_OBSERVABLE_TYPES, - fieldName: 'observableTypes', - codec: z.object({ - key: regexStringSchema({ - codec: limitedStringSchema({ - fieldName: 'key', - min: 1, - max: MAX_OBSERVABLE_TYPE_KEY_LENGTH, - }), - pattern: '^[a-z0-9_-]+$', - message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`, - }), - label: limitedStringSchema({ - fieldName: 'label', - min: 1, - max: MAX_OBSERVABLE_TYPE_LABEL_LENGTH, - }), - }), -}); - -export const TemplateConfigurationSchema = z.object({ - key: regexStringSchema({ - codec: limitedStringSchema({ fieldName: 'key', min: 1, max: MAX_TEMPLATE_KEY_LENGTH }), - pattern: '^[a-z0-9_-]+$', - message: `Key must be lower case, a-z, 0-9, '_', and '-' are allowed`, - }), - name: limitedStringSchema({ fieldName: 'name', min: 1, max: MAX_TEMPLATE_NAME_LENGTH }), - caseFields: CaseBaseOptionalFieldsRequestSchema.nullable(), - description: limitedStringSchema({ - fieldName: 'description', - min: 0, - max: MAX_TEMPLATE_DESCRIPTION_LENGTH, - }).optional(), - tags: limitedArraySchema({ - codec: limitedStringSchema({ - fieldName: `template's tag`, - min: 1, - max: MAX_TEMPLATE_TAG_LENGTH, - }), - min: 0, - max: MAX_TAGS_PER_TEMPLATE, - fieldName: `template's tags`, - }).optional(), -}); - -export const TemplatesConfigurationSchema = limitedArraySchema({ - codec: TemplateConfigurationSchema, - min: 0, - max: MAX_TEMPLATES_LENGTH, - fieldName: 'templates', -}); - -export const ConfigurationRequestSchema = z.object({ - connector: CaseConnectorSchema, - closure_type: ClosureTypeSchema, - owner: z.string(), - customFields: CustomFieldsConfigurationSchema.optional(), - templates: TemplatesConfigurationSchema.optional(), - observableTypes: ObservableTypesConfigurationSchema.optional(), -}); - -export const GetConfigurationFindRequestSchema = z.object({ - owner: z.union([z.array(z.string()), z.string()]).optional(), -}); - -export const CaseConfigureRequestParamsSchema = z.object({ - configuration_id: z.string(), -}); - -export const ConfigurationPatchRequestSchema = z.object({ - closure_type: ClosureTypeSchema.optional(), - connector: ConfigurationBasicWithoutOwnerSchema.shape.connector.optional(), - customFields: CustomFieldsConfigurationSchema.optional(), - templates: TemplatesConfigurationSchema.optional(), - observableTypes: ObservableTypesConfigurationSchema.optional(), - version: z.string(), -}); - -export type ConfigurationRequest = z.infer; -export type ConfigurationPatchRequest = z.infer; -export type GetConfigurationFindRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/connector/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/connector/v1.ts deleted file mode 100644 index 869a17ce0b298..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/connector/v1.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { ExternalServiceSchema } from '../../domain_zod/external_service/v1'; -import { CaseConnectorSchema, ConnectorMappingsSchema } from '../../domain_zod/connector/v1'; - -const PushDetailsSchema = z.object({ - latestUserActionPushDate: z.string(), - oldestUserActionPushDate: z.string(), - externalService: ExternalServiceSchema, -}); - -const CaseConnectorPushInfoSchema = z.object({ - needsToBePushed: z.boolean(), - hasBeenPushed: z.boolean(), - details: PushDetailsSchema.optional(), -}); - -export const GetCaseConnectorsResponseSchema = z.record( - z.string(), - CaseConnectorSchema.and(z.object({ push: CaseConnectorPushInfoSchema })) -); - -const ActionConnectorResultSchema = z.object({ - id: z.string(), - actionTypeId: z.string(), - name: z.string(), - isDeprecated: z.boolean(), - isPreconfigured: z.boolean(), - isSystemAction: z.boolean(), - referencedByCount: z.number(), - isConnectorTypeDeprecated: z.boolean(), - config: z.record(z.string(), z.unknown()).optional(), - isMissingSecrets: z.boolean().optional(), -}); - -export const FindActionConnectorResponseSchema = z.array(ActionConnectorResultSchema); - -export const ConnectorMappingResponseSchema = z.object({ - id: z.string(), - version: z.string(), - mappings: ConnectorMappingsSchema, -}); - -export type ConnectorMappingResponse = z.infer; -export type GetCaseConnectorsResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/custom_field/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/custom_field/v1.ts deleted file mode 100644 index 80c7c075da520..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/custom_field/v1.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH } from '../../../constants'; -import { limitedStringSchema, limitedNumberAsIntegerSchema } from '../../../schema_zod'; - -export const CaseCustomFieldTextWithValidationValueSchema = (fieldName: string) => - limitedStringSchema({ fieldName, min: 1, max: MAX_CUSTOM_FIELD_TEXT_VALUE_LENGTH }); - -export const CaseCustomFieldNumberWithValidationValueSchema = ({ - fieldName, -}: { - fieldName: string; -}) => limitedNumberAsIntegerSchema({ fieldName }); - -export const CustomFieldPutRequestSchema = z.object({ - value: z.union([ - z.boolean(), - z.null(), - CaseCustomFieldTextWithValidationValueSchema('value'), - CaseCustomFieldNumberWithValidationValueSchema({ fieldName: 'value' }), - ]), - caseVersion: z.string(), -}); - -export type CustomFieldPutRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/document/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/document/v1.ts deleted file mode 100644 index c0628a7b82623..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/document/v1.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; - -const DocumentSchema = z.object({ - id: z.string(), - index: z.string(), - attached_at: z.string(), -}); - -export const DocumentResponseSchema = z.array(DocumentSchema); - -export type DocumentResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/external_service/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/external_service/v1.ts deleted file mode 100644 index 2a8cb66689aaa..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/external_service/v1.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; - -export const ExternalServiceResponseSchema = z.object({ - title: z.string(), - id: z.string(), - pushedDate: z.string(), - url: z.string(), - comments: z - .array( - z.object({ - commentId: z.string(), - pushedDate: z.string(), - externalCommentId: z.string().optional(), - }) - ) - .optional(), -}); - -export type ExternalServiceResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/metrics/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/metrics/v1.ts deleted file mode 100644 index 415941216a1de..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/metrics/v1.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; - -export enum CaseMetricsFeature { - ALERTS_COUNT = 'alerts.count', - ALERTS_USERS = 'alerts.users', - ALERTS_HOSTS = 'alerts.hosts', - ACTIONS_ISOLATE_HOST = 'actions.isolateHost', - CONNECTORS = 'connectors', - LIFESPAN = 'lifespan', - MTTR = 'mttr', - STATUS = 'status', -} - -export const SingleCaseMetricsFeatureFieldSchema = z.union([ - z.literal(CaseMetricsFeature.ALERTS_COUNT), - z.literal(CaseMetricsFeature.ALERTS_USERS), - z.literal(CaseMetricsFeature.ALERTS_HOSTS), - z.literal(CaseMetricsFeature.ACTIONS_ISOLATE_HOST), - z.literal(CaseMetricsFeature.CONNECTORS), - z.literal(CaseMetricsFeature.LIFESPAN), -]); - -export const CasesMetricsFeatureFieldSchema = z.union([ - SingleCaseMetricsFeatureFieldSchema, - z.literal(CaseMetricsFeature.MTTR), - z.literal(CaseMetricsFeature.STATUS), -]); - -const StatusInfoSchema = z.object({ - openDuration: z.number(), - inProgressDuration: z.number(), - reopenDates: z.array(z.string()), -}); - -const AlertHostsMetricsSchema = z.object({ - total: z.number(), - values: z.array( - z.object({ - name: z.string().optional(), - id: z.string(), - count: z.number(), - }) - ), -}); - -const AlertUsersMetricsSchema = z.object({ - total: z.number(), - values: z.array( - z.object({ - name: z.string(), - count: z.number(), - }) - ), -}); - -export const SingleCaseMetricsRequestSchema = z.object({ - features: z.array(SingleCaseMetricsFeatureFieldSchema), -}); - -export const CasesMetricsRequestSchema = z.object({ - features: z.array(CasesMetricsFeatureFieldSchema), - from: z.string().optional(), - to: z.string().optional(), - owner: z.union([z.array(z.string()), z.string()]).optional(), -}); - -export const SingleCaseMetricsResponseSchema = z.object({ - alerts: z - .object({ - count: z.number().optional(), - hosts: AlertHostsMetricsSchema.optional(), - users: AlertUsersMetricsSchema.optional(), - }) - .optional(), - connectors: z.object({ total: z.number() }).optional(), - actions: z - .object({ - isolateHost: z - .object({ - isolate: z.object({ total: z.number() }), - unisolate: z.object({ total: z.number() }), - }) - .optional(), - }) - .optional(), - lifespan: z - .object({ - creationDate: z.string(), - closeDate: z.string().nullable(), - statusInfo: StatusInfoSchema, - }) - .optional(), -}); - -export const CasesMetricsResponseSchema = z.object({ - mttr: z.number().nullable().optional(), - status: z.object({ open: z.number(), inProgress: z.number(), closed: z.number() }).optional(), -}); - -export type SingleCaseMetricsRequest = z.infer; -export type CasesMetricsRequest = z.infer; -export type SingleCaseMetricsResponse = z.infer; -export type CasesMetricsResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/observable/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/observable/v1.ts deleted file mode 100644 index b51c3a4487fa8..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/observable/v1.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseObservableBaseSchema } from '../../domain_zod/observable/v1'; - -export const ObservablePostSchema = CaseObservableBaseSchema; - -export const ObservablePatchSchema = z.object({ - value: z.string(), - description: z.string().nullable(), -}); - -export const AddObservableRequestSchema = z.object({ - observable: ObservablePostSchema, -}); - -export const UpdateObservableRequestSchema = z.object({ - observable: ObservablePatchSchema, -}); - -export const BulkAddObservablesRequestSchema = z.object({ - caseId: z.string(), - observables: z.array(ObservablePostSchema), -}); - -export type ObservablePost = z.infer; -export type ObservablePatch = z.infer; -export type AddObservableRequest = z.infer; -export type UpdateObservableRequest = z.infer; -export type BulkAddObservablesRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/stats/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/stats/v1.ts deleted file mode 100644 index 1183cabb962ab..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/stats/v1.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; - -export const CasesStatusResponseSchema = z.object({ - count_open_cases: z.number(), - count_in_progress_cases: z.number(), - count_closed_cases: z.number(), -}); - -export const CasesStatusRequestSchema = z.object({ - from: z.string().optional(), - to: z.string().optional(), - owner: z.union([z.array(z.string()), z.string()]).optional(), -}); - -export type CasesStatusResponse = z.infer; -export type CasesStatusRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/user/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/user/v1.ts deleted file mode 100644 index 29f4af180e929..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/user/v1.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { MAX_SUGGESTED_PROFILES } from '../../../constants'; -import { limitedNumberSchema } from '../../../schema_zod'; -import { UserWithProfileInfoSchema } from '../../domain_zod/user/v1'; - -export const GetCaseUsersResponseSchema = z.object({ - assignees: z.array(UserWithProfileInfoSchema), - unassignedUsers: z.array(UserWithProfileInfoSchema), - participants: z.array(UserWithProfileInfoSchema), - reporter: UserWithProfileInfoSchema, -}); - -export const SuggestUserProfilesRequestSchema = z.object({ - name: z.string(), - owners: z.array(z.string()), - size: limitedNumberSchema({ fieldName: 'size', min: 1, max: MAX_SUGGESTED_PROFILES }).optional(), -}); - -export type GetCaseUsersResponse = z.infer; -export type SuggestUserProfilesRequest = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/api_zod/user_action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/api_zod/user_action/v1.ts deleted file mode 100644 index 68e696cdecdca..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/api_zod/user_action/v1.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { MAX_USER_ACTIONS_PER_PAGE } from '../../../constants'; -import { paginationSchema } from '../../../schema_zod'; -import { UserActionsSchema } from '../../domain_zod/user_action/v1'; -import { UserActionTypes } from '../../domain/user_action/action/v1'; - -const UserActionAdditionalFindRequestFilterTypes = { - action: 'action', - alert: 'alert', - user: 'user', - attachment: 'attachment', -} as const; - -const UserActionFindRequestTypes = { - ...UserActionTypes, - ...UserActionAdditionalFindRequestFilterTypes, -} as const; - -const UserActionFindRequestTypesValues = Object.values(UserActionFindRequestTypes) as [ - string, - ...string[] -]; - -export const CaseUserActionStatsSchema = z.object({ - total: z.number(), - total_deletions: z.number(), - total_comments: z.number(), - total_comment_deletions: z.number(), - total_comment_creations: z.number(), - total_hidden_comment_updates: z.number(), - total_other_actions: z.number(), - total_other_action_deletions: z.number(), -}); - -export const UserActionFindRequestSchema = paginationSchema({ - maxPerPage: MAX_USER_ACTIONS_PER_PAGE, -}).extend({ - types: z.array(z.enum(UserActionFindRequestTypesValues)).optional(), - sortOrder: z.enum(['desc', 'asc']).optional(), -}); - -export const UserActionFindResponseSchema = z.object({ - userActions: UserActionsSchema, - page: z.number(), - perPage: z.number(), - total: z.number(), -}); - -export type CaseUserActionStats = z.infer; -export type UserActionFindRequest = z.infer; -export type UserActionFindResponse = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.test.ts index 60f20c8155156..e2ce9195919f3 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.test.ts @@ -6,39 +6,28 @@ */ import { - AttachmentAttributesBasicRt, - FileAttachmentMetadataRt, - SingleFileAttachmentMetadataRt, AttachmentType, - UserCommentAttachmentPayloadRt, - AlertAttachmentPayloadRt, - ActionsAttachmentPayloadRt, ExternalReferenceStorageType, - ExternalReferenceAttachmentPayloadRt, - PersistableStateAttachmentPayloadRt, - AttachmentRt, - UserCommentAttachmentRt, - AlertAttachmentRt, - ActionsAttachmentRt, - ExternalReferenceAttachmentRt, - PersistableStateAttachmentRt, - AttachmentPatchAttributesRt, -} from './v1'; -import { SingleFileAttachmentMetadataSchema, FileAttachmentMetadataSchema, AttachmentAttributesBasicSchema, UserCommentAttachmentPayloadSchema, + UserCommentAttachmentSchema, AlertAttachmentPayloadSchema, + AlertAttachmentSchema, ActionsAttachmentPayloadSchema, + ActionsAttachmentSchema, ExternalReferenceAttachmentPayloadSchema, + ExternalReferenceAttachmentSchema, PersistableStateAttachmentPayloadSchema, + PersistableStateAttachmentSchema, AttachmentSchema, -} from '../../domain_zod/attachment/v1'; + AttachmentPatchAttributesSchema, +} from './v1'; describe('Attachments', () => { describe('Files', () => { - describe('SingleFileAttachmentMetadataRt', () => { + describe('SingleFileAttachmentMetadataSchema', () => { const defaultRequest = { created: '2020-02-19T23:06:33.798Z', extension: 'png', @@ -47,30 +36,12 @@ describe('Attachments', () => { }; it('has expected attributes in request', () => { - const query = SingleFileAttachmentMetadataRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = SingleFileAttachmentMetadataRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = SingleFileAttachmentMetadataSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = SingleFileAttachmentMetadataSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -80,7 +51,7 @@ describe('Attachments', () => { }); }); - describe('FileAttachmentMetadataRt', () => { + describe('FileAttachmentMetadataSchema', () => { const defaultRequest = { created: '2020-02-19T23:06:33.798Z', extension: 'png', @@ -89,44 +60,12 @@ describe('Attachments', () => { }; it('has expected attributes in request', () => { - const query = FileAttachmentMetadataRt.decode({ files: [defaultRequest] }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - files: [ - { - ...defaultRequest, - }, - ], - }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = FileAttachmentMetadataRt.decode({ - files: [{ ...defaultRequest, foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - files: [ - { - ...defaultRequest, - }, - ], - }, - }); - }); - - it('zod: has expected attributes in request', () => { const result = FileAttachmentMetadataSchema.safeParse({ files: [defaultRequest] }); expect(result.success).toBe(true); expect(result.data).toStrictEqual({ files: [defaultRequest] }); }); - it('zod: strips unknown fields from files', () => { + it('strips unknown fields from files', () => { const result = FileAttachmentMetadataSchema.safeParse({ files: [{ ...defaultRequest, foo: 'bar' }], }); @@ -136,7 +75,7 @@ describe('Attachments', () => { }); }); - describe('AttachmentAttributesBasicRt', () => { + describe('AttachmentAttributesBasicSchema', () => { const defaultRequest = { created_at: '2019-11-25T22:32:30.608Z', created_by: { @@ -152,37 +91,19 @@ describe('Attachments', () => { }; it('has expected attributes in request', () => { - const query = AttachmentAttributesBasicRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = AttachmentAttributesBasicRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = AttachmentAttributesBasicSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = AttachmentAttributesBasicSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('UserCommentAttachmentPayloadRt', () => { + describe('UserCommentAttachmentPayloadSchema', () => { const defaultRequest = { comment: 'This is a sample comment', type: AttachmentType.user, @@ -190,30 +111,12 @@ describe('Attachments', () => { }; it('has expected attributes in request', () => { - const query = UserCommentAttachmentPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = UserCommentAttachmentPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = UserCommentAttachmentPayloadSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = UserCommentAttachmentPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -223,7 +126,7 @@ describe('Attachments', () => { }); }); - describe('AlertAttachmentPayloadRt', () => { + describe('AlertAttachmentPayloadSchema', () => { const defaultRequest = { alertId: 'alert-id-1', index: 'alert-index-1', @@ -236,48 +139,18 @@ describe('Attachments', () => { }; it('has expected attributes in request', () => { - const query = AlertAttachmentPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = AlertAttachmentPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from rule', () => { - const query = AlertAttachmentPayloadRt.decode({ - ...defaultRequest, - rule: { id: 'rule-id-1', name: 'Awesome rule', foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = AlertAttachmentPayloadSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = AlertAttachmentPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields from rule', () => { + it('strips unknown fields from rule', () => { const result = AlertAttachmentPayloadSchema.safeParse({ ...defaultRequest, rule: { id: 'rule-id-1', name: 'Awesome rule', foo: 'bar' }, @@ -287,7 +160,7 @@ describe('Attachments', () => { }); }); - describe('ActionsAttachmentPayloadRt', () => { + describe('ActionsAttachmentPayloadSchema', () => { const defaultRequest = { type: AttachmentType.actions, comment: 'I just isolated the host!', @@ -304,79 +177,19 @@ describe('Attachments', () => { }; it('has expected attributes in request', () => { - const query = ActionsAttachmentPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ActionsAttachmentPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from actions', () => { - const query = ActionsAttachmentPayloadRt.decode({ - ...defaultRequest, - actions: { - targets: [ - { - hostname: 'host1', - endpointId: '001', - }, - ], - type: 'isolate', - foo: 'bar', - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from targets', () => { - const query = ActionsAttachmentPayloadRt.decode({ - ...defaultRequest, - actions: { - targets: [ - { - hostname: 'host1', - endpointId: '001', - foo: 'bar', - }, - ], - type: 'isolate', - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ActionsAttachmentPayloadSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ActionsAttachmentPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('ExternalReferenceAttachmentPayloadRt', () => { + describe('ExternalReferenceAttachmentPayloadSchema', () => { const defaultRequest = { type: AttachmentType.externalReference, externalReferenceId: 'my-id', @@ -385,68 +198,14 @@ describe('Attachments', () => { externalReferenceMetadata: { test_foo: 'foo' }, owner: 'cases', }; - it('has expected attributes in request', () => { - const query = ExternalReferenceAttachmentPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - it('removes foo:bar attributes from request', () => { - const query = ExternalReferenceAttachmentPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from externalReferenceStorage', () => { - const query = ExternalReferenceAttachmentPayloadRt.decode({ - ...defaultRequest, - externalReferenceStorage: { - type: ExternalReferenceStorageType.elasticSearchDoc, - foo: 'bar', - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from externalReferenceStorage with soType', () => { - const query = ExternalReferenceAttachmentPayloadRt.decode({ - ...defaultRequest, - externalReferenceStorage: { - type: ExternalReferenceStorageType.savedObject, - soType: 'awesome', - foo: 'bar', - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - externalReferenceStorage: { - type: ExternalReferenceStorageType.savedObject, - soType: 'awesome', - }, - }, - }); - }); - - it('zod: has expected attributes in request', () => { + it('has expected attributes in request', () => { const result = ExternalReferenceAttachmentPayloadSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ExternalReferenceAttachmentPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -456,53 +215,21 @@ describe('Attachments', () => { }); }); - describe('PersistableStateAttachmentPayloadRt', () => { + describe('PersistableStateAttachmentPayloadSchema', () => { const defaultRequest = { type: AttachmentType.persistableState, persistableStateAttachmentState: { test_foo: 'foo' }, persistableStateAttachmentTypeId: '.test', owner: 'cases', }; - it('has expected attributes in request', () => { - const query = PersistableStateAttachmentPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - it('removes foo:bar attributes from request', () => { - const query = PersistableStateAttachmentPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from persistableStateAttachmentState', () => { - const query = PersistableStateAttachmentPayloadRt.decode({ - ...defaultRequest, - persistableStateAttachmentState: { test_foo: 'foo', foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - persistableStateAttachmentState: { test_foo: 'foo', foo: 'bar' }, - }, - }); - }); - - it('zod: has expected attributes in request', () => { + it('has expected attributes in request', () => { const result = PersistableStateAttachmentPayloadSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = PersistableStateAttachmentPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -512,7 +239,7 @@ describe('Attachments', () => { }); }); - describe('AttachmentRt', () => { + describe('AttachmentSchema', () => { const defaultRequest = { comment: 'Solve this fast!', type: AttachmentType.user, @@ -530,38 +257,21 @@ describe('Attachments', () => { updated_at: null, updated_by: null, }; - it('has expected attributes in request', () => { - const query = AttachmentRt.decode(defaultRequest); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = AttachmentRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { + it('has expected attributes in request', () => { const result = AttachmentSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = AttachmentSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('UserCommentAttachmentRt', () => { + describe('UserCommentAttachmentSchema', () => { const defaultRequest = { comment: 'Solve this fast!', type: AttachmentType.user, @@ -579,36 +289,28 @@ describe('Attachments', () => { updated_at: null, updated_by: null, }; - it('has expected attributes in request', () => { - const query = UserCommentAttachmentRt.decode(defaultRequest); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('has expected attributes in request', () => { + const result = UserCommentAttachmentSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = UserCommentAttachmentRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = UserCommentAttachmentSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('AlertAttachmentRt', () => { + describe('AlertAttachmentSchema', () => { const defaultRequest = { alertId: 'alert-id-1', index: 'alert-index-1', type: AttachmentType.alert, id: 'alert-comment-id', owner: 'cases', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - }, + rule: { id: 'rule-id-1', name: 'Awesome rule' }, version: 'WzQ3LDFc', created_at: '2020-02-19T23:06:33.798Z', created_by: { @@ -621,53 +323,35 @@ describe('Attachments', () => { updated_at: null, updated_by: null, }; - it('has expected attributes in request', () => { - const query = AlertAttachmentRt.decode(defaultRequest); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('has expected attributes in request', () => { + const result = AlertAttachmentSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = AlertAttachmentRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = AlertAttachmentSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from created_by', () => { - const query = AlertAttachmentRt.decode({ + it('strips unknown fields from created_by', () => { + const result = AlertAttachmentSchema.safeParse({ ...defaultRequest, - created_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - foo: 'bar', - }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + created_by: { ...defaultRequest.created_by, foo: 'bar' }, }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('ActionsAttachmentRt', () => { + describe('ActionsAttachmentSchema', () => { const defaultRequest = { type: AttachmentType.actions, comment: 'I just isolated the host!', actions: { - targets: [ - { - hostname: 'host1', - endpointId: '001', - }, - ], + targets: [{ hostname: 'host1', endpointId: '001' }], type: 'isolate', }, owner: 'cases', @@ -684,26 +368,21 @@ describe('Attachments', () => { updated_at: null, updated_by: null, }; - it('has expected attributes in request', () => { - const query = ActionsAttachmentRt.decode(defaultRequest); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('has expected attributes in request', () => { + const result = ActionsAttachmentSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = ActionsAttachmentRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = ActionsAttachmentSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('ExternalReferenceAttachmentRt', () => { + describe('ExternalReferenceAttachmentSchema', () => { const defaultRequest = { type: AttachmentType.externalReference, externalReferenceId: 'my-id', @@ -724,29 +403,21 @@ describe('Attachments', () => { updated_at: null, updated_by: null, }; - it('has expected attributes in request', () => { - const query = ExternalReferenceAttachmentRt.decode(defaultRequest); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('has expected attributes in request', () => { + const result = ExternalReferenceAttachmentSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = ExternalReferenceAttachmentRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = ExternalReferenceAttachmentSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('PersistableStateAttachmentRt', () => { + describe('PersistableStateAttachmentSchema', () => { const defaultRequest = { type: AttachmentType.persistableState, persistableStateAttachmentState: { test_foo: 'foo' }, @@ -765,58 +436,40 @@ describe('Attachments', () => { updated_at: null, updated_by: null, }; - it('has expected attributes in request', () => { - const query = PersistableStateAttachmentRt.decode(defaultRequest); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('has expected attributes in request', () => { + const result = PersistableStateAttachmentSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = PersistableStateAttachmentRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = PersistableStateAttachmentSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('AttachmentPatchAttributesRt', () => { + describe('AttachmentPatchAttributesSchema', () => { const defaultRequest = { type: AttachmentType.actions, actions: { - targets: [ - { - hostname: 'host1', - endpointId: '001', - }, - ], + targets: [{ hostname: 'host1', endpointId: '001' }], type: 'isolate', }, owner: 'cases', }; - it('has expected attributes in request', () => { - const query = AttachmentPatchAttributesRt.decode(defaultRequest); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('has expected attributes in request', () => { + const result = AttachmentPatchAttributesSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = AttachmentPatchAttributesRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = AttachmentPatchAttributesSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts index 235ff30378672..bc288fdbb2c4e 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v1.ts @@ -5,39 +5,52 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { limitedStringSchema, mimeTypeString } from '../../../schema'; -import { jsonValueRt } from '../../../api'; -import { UserRt } from '../user/v1'; +import { z } from '@kbn/zod/v4'; +import { limitedStringSchema, mimeTypeString, jsonValueSchema } from '../../../schema'; +import { UserSchema } from '../user/v1'; import { MAX_FILENAME_LENGTH } from '../../../constants'; +export enum AttachmentType { + actions = 'actions', + alert = 'alert', + event = 'event', + externalReference = 'externalReference', + persistableState = 'persistableState', + user = 'user', +} + +export enum ExternalReferenceStorageType { + savedObject = 'savedObject', + elasticSearchDoc = 'elasticSearchDoc', +} + /** * Files */ -export const SingleFileAttachmentMetadataRt = rt.strict({ - name: rt.string, - extension: rt.string, - mimeType: rt.string, - created: rt.string, +export const SingleFileAttachmentMetadataSchema = z.object({ + name: z.string(), + extension: z.string(), + mimeType: z.string(), + created: z.string(), }); -export const FileAttachmentMetadataRt = rt.strict({ - files: rt.array(SingleFileAttachmentMetadataRt), +export const FileAttachmentMetadataSchema = z.object({ + files: z.array(SingleFileAttachmentMetadataSchema), }); -export type FileAttachmentMetadata = rt.TypeOf; +export type FileAttachmentMetadata = z.infer; -export const AttachmentAttributesBasicRt = rt.strict({ - created_at: rt.string, - created_by: UserRt, - owner: rt.string, - pushed_at: rt.union([rt.string, rt.null]), - pushed_by: rt.union([UserRt, rt.null]), - updated_at: rt.union([rt.string, rt.null]), - updated_by: rt.union([UserRt, rt.null]), +export const AttachmentAttributesBasicSchema = z.object({ + created_at: z.string(), + created_by: UserSchema, + owner: z.string(), + pushed_at: z.string().nullable(), + pushed_by: UserSchema.nullable(), + updated_at: z.string().nullable(), + updated_by: UserSchema.nullable(), }); -export const FileAttachmentMetadataPayloadRt = rt.strict({ +export const FileAttachmentMetadataPayloadSchema = z.object({ mimeType: mimeTypeString, filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }), }); @@ -45,337 +58,275 @@ export const FileAttachmentMetadataPayloadRt = rt.strict({ /** * User comment */ - -export enum AttachmentType { - actions = 'actions', - alert = 'alert', - event = 'event', - externalReference = 'externalReference', - persistableState = 'persistableState', - user = 'user', -} - -export const UserCommentAttachmentPayloadRt = rt.strict({ - comment: rt.string, - type: rt.literal(AttachmentType.user), - owner: rt.string, +export const UserCommentAttachmentPayloadSchema = z.object({ + comment: z.string(), + type: z.literal(AttachmentType.user), + owner: z.string(), }); -const UserCommentAttachmentAttributesRt = rt.intersection([ - UserCommentAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +const UserCommentAttachmentAttributesSchema = UserCommentAttachmentPayloadSchema.merge( + AttachmentAttributesBasicSchema +); -export const UserCommentAttachmentRt = rt.intersection([ - UserCommentAttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const UserCommentAttachmentSchema = UserCommentAttachmentAttributesSchema.extend({ + id: z.string(), + version: z.string(), +}); -export type UserCommentAttachmentPayload = rt.TypeOf; -export type UserCommentAttachmentAttributes = rt.TypeOf; -export type UserCommentAttachment = rt.TypeOf; +export type UserCommentAttachmentPayload = z.infer; +export type UserCommentAttachmentAttributes = z.infer; +export type UserCommentAttachment = z.infer; /** * Generic event */ - -export const EventAttachmentPayloadRt = rt.strict({ - type: rt.literal(AttachmentType.event), - eventId: rt.union([rt.array(rt.string), rt.string]), - index: rt.union([rt.array(rt.string), rt.string]), - owner: rt.string, +export const EventAttachmentPayloadSchema = z.object({ + type: z.literal(AttachmentType.event), + eventId: z.union([z.array(z.string()), z.string()]), + index: z.union([z.array(z.string()), z.string()]), + owner: z.string(), }); /** * Alerts */ - -export const AlertAttachmentPayloadRt = rt.strict({ - type: rt.literal(AttachmentType.alert), - alertId: rt.union([rt.array(rt.string), rt.string]), - index: rt.union([rt.array(rt.string), rt.string]), - rule: rt.strict({ - id: rt.union([rt.string, rt.null]), - name: rt.union([rt.string, rt.null]), +export const AlertAttachmentPayloadSchema = z.object({ + type: z.literal(AttachmentType.alert), + alertId: z.union([z.array(z.string()), z.string()]), + index: z.union([z.array(z.string()), z.string()]), + rule: z.object({ + id: z.string().nullable(), + name: z.string().nullable(), }), - owner: rt.string, + owner: z.string(), }); -export const AlertAttachmentAttributesRt = rt.intersection([ - AlertAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +export const AlertAttachmentAttributesSchema = AlertAttachmentPayloadSchema.merge( + AttachmentAttributesBasicSchema +); -export const EventAttachmentAttributesRt = rt.intersection([ - EventAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +export const EventAttachmentAttributesSchema = EventAttachmentPayloadSchema.merge( + AttachmentAttributesBasicSchema +); -export const AlertAttachmentRt = rt.intersection([ - AlertAttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const AlertAttachmentSchema = AlertAttachmentAttributesSchema.extend({ + id: z.string(), + version: z.string(), +}); -export const EventAttachmentRt = rt.intersection([ - EventAttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const EventAttachmentSchema = EventAttachmentAttributesSchema.extend({ + id: z.string(), + version: z.string(), +}); -export type AlertAttachmentPayload = rt.TypeOf; -export type AlertAttachmentAttributes = rt.TypeOf; -export type AlertAttachment = rt.TypeOf; +export type AlertAttachmentPayload = z.infer; +export type AlertAttachmentAttributes = z.infer; +export type AlertAttachment = z.infer; -export type EventAttachmentPayload = rt.TypeOf; -export type EventAttachmentAttributes = rt.TypeOf; -export type EventAttachment = rt.TypeOf; +export type EventAttachmentPayload = z.infer; +export type EventAttachmentAttributes = z.infer; +export type EventAttachment = z.infer; -export const DocumentAttachmentAttributesRt = rt.union([ - AlertAttachmentAttributesRt, - EventAttachmentAttributesRt, +export const DocumentAttachmentAttributesSchema = z.union([ + AlertAttachmentAttributesSchema, + EventAttachmentAttributesSchema, ]); -export type DocumentAttachmentAttributes = rt.TypeOf; +export type DocumentAttachmentAttributes = z.infer; /** * Actions */ - export enum IsolateHostActionType { isolate = 'isolate', unisolate = 'unisolate', } -export const ActionsAttachmentPayloadRt = rt.strict({ - type: rt.literal(AttachmentType.actions), - comment: rt.string, - actions: rt.strict({ - targets: rt.array( - rt.strict({ - hostname: rt.string, - endpointId: rt.string, +export const ActionsAttachmentPayloadSchema = z.object({ + type: z.literal(AttachmentType.actions), + comment: z.string(), + actions: z.object({ + targets: z.array( + z.object({ + hostname: z.string(), + endpointId: z.string(), }) ), - type: rt.string, + type: z.string(), }), - owner: rt.string, + owner: z.string(), }); -const ActionsAttachmentAttributesRt = rt.intersection([ - ActionsAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +const ActionsAttachmentAttributesSchema = ActionsAttachmentPayloadSchema.merge( + AttachmentAttributesBasicSchema +); -export const ActionsAttachmentRt = rt.intersection([ - ActionsAttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const ActionsAttachmentSchema = ActionsAttachmentAttributesSchema.extend({ + id: z.string(), + version: z.string(), +}); -export type ActionsAttachmentPayload = rt.TypeOf; -export type ActionsAttachmentAttributes = rt.TypeOf; -export type ActionsAttachment = rt.TypeOf; +export type ActionsAttachmentPayload = z.infer; +export type ActionsAttachmentAttributes = z.infer; +export type ActionsAttachment = z.infer; /** * External reference */ - -export enum ExternalReferenceStorageType { - savedObject = 'savedObject', - elasticSearchDoc = 'elasticSearchDoc', -} - -const ExternalReferenceStorageNoSORt = rt.strict({ - type: rt.literal(ExternalReferenceStorageType.elasticSearchDoc), -}); - -const ExternalReferenceStorageSORt = rt.strict({ - type: rt.literal(ExternalReferenceStorageType.savedObject), - soType: rt.string, -}); - -const ExternalReferenceBaseAttachmentPayloadRt = rt.strict({ - externalReferenceAttachmentTypeId: rt.string, - externalReferenceMetadata: rt.union([rt.null, rt.record(rt.string, jsonValueRt)]), - type: rt.literal(AttachmentType.externalReference), - owner: rt.string, +const ExternalReferenceStorageNoSOSchema = z.object({ + type: z.literal(ExternalReferenceStorageType.elasticSearchDoc), }); -export const ExternalReferenceNoSOAttachmentPayloadRt = rt.strict({ - ...ExternalReferenceBaseAttachmentPayloadRt.type.props, - externalReferenceId: rt.string, - externalReferenceStorage: ExternalReferenceStorageNoSORt, +const ExternalReferenceStorageSOSchema = z.object({ + type: z.literal(ExternalReferenceStorageType.savedObject), + soType: z.string(), }); -export const ExternalReferenceSOAttachmentPayloadRt = rt.strict({ - ...ExternalReferenceBaseAttachmentPayloadRt.type.props, - externalReferenceId: rt.string, - externalReferenceStorage: ExternalReferenceStorageSORt, +const ExternalReferenceBaseAttachmentPayloadSchema = z.object({ + externalReferenceAttachmentTypeId: z.string(), + externalReferenceMetadata: z.record(z.string(), jsonValueSchema).nullable(), + type: z.literal(AttachmentType.externalReference), + owner: z.string(), }); -// externalReferenceId is missing. -export const ExternalReferenceSOWithoutRefsAttachmentPayloadRt = rt.strict({ - ...ExternalReferenceBaseAttachmentPayloadRt.type.props, - externalReferenceStorage: ExternalReferenceStorageSORt, -}); - -export const ExternalReferenceAttachmentPayloadRt = rt.union([ - ExternalReferenceNoSOAttachmentPayloadRt, - ExternalReferenceSOAttachmentPayloadRt, +export const ExternalReferenceNoSOAttachmentPayloadSchema = + ExternalReferenceBaseAttachmentPayloadSchema.extend({ + externalReferenceId: z.string(), + externalReferenceStorage: ExternalReferenceStorageNoSOSchema, + }); + +export const ExternalReferenceSOAttachmentPayloadSchema = + ExternalReferenceBaseAttachmentPayloadSchema.extend({ + externalReferenceId: z.string(), + externalReferenceStorage: ExternalReferenceStorageSOSchema, + }); + +export const ExternalReferenceSOWithoutRefsAttachmentPayloadSchema = + ExternalReferenceBaseAttachmentPayloadSchema.extend({ + externalReferenceStorage: ExternalReferenceStorageSOSchema, + }); + +export const ExternalReferenceAttachmentPayloadSchema = z.union([ + ExternalReferenceNoSOAttachmentPayloadSchema, + ExternalReferenceSOAttachmentPayloadSchema, ]); -export const ExternalReferenceWithoutRefsAttachmentPayloadRt = rt.union([ - ExternalReferenceNoSOAttachmentPayloadRt, - ExternalReferenceSOWithoutRefsAttachmentPayloadRt, +export const ExternalReferenceWithoutRefsAttachmentPayloadSchema = z.union([ + ExternalReferenceNoSOAttachmentPayloadSchema, + ExternalReferenceSOWithoutRefsAttachmentPayloadSchema, ]); -const ExternalReferenceAttachmentAttributesRt = rt.intersection([ - ExternalReferenceAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +const ExternalReferenceAttachmentAttributesSchema = ExternalReferenceAttachmentPayloadSchema.and( + AttachmentAttributesBasicSchema +); -const ExternalReferenceWithoutRefsAttachmentAttributesRt = rt.intersection([ - ExternalReferenceWithoutRefsAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +const ExternalReferenceWithoutRefsAttachmentAttributesSchema = + ExternalReferenceWithoutRefsAttachmentPayloadSchema.and(AttachmentAttributesBasicSchema); -const ExternalReferenceNoSOAttachmentAttributesRt = rt.intersection([ - ExternalReferenceNoSOAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +const ExternalReferenceNoSOAttachmentAttributesSchema = + ExternalReferenceNoSOAttachmentPayloadSchema.merge(AttachmentAttributesBasicSchema); -const ExternalReferenceSOAttachmentAttributesRt = rt.intersection([ - ExternalReferenceSOAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +const ExternalReferenceSOAttachmentAttributesSchema = + ExternalReferenceSOAttachmentPayloadSchema.merge(AttachmentAttributesBasicSchema); -export const ExternalReferenceAttachmentRt = rt.intersection([ - ExternalReferenceAttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const ExternalReferenceAttachmentSchema = ExternalReferenceAttachmentAttributesSchema.and( + z.object({ id: z.string(), version: z.string() }) +); -export type ExternalReferenceAttachmentPayload = rt.TypeOf< - typeof ExternalReferenceAttachmentPayloadRt +export type ExternalReferenceAttachmentPayload = z.infer< + typeof ExternalReferenceAttachmentPayloadSchema >; - -export type ExternalReferenceSOAttachmentPayload = rt.TypeOf< - typeof ExternalReferenceSOAttachmentPayloadRt +export type ExternalReferenceSOAttachmentPayload = z.infer< + typeof ExternalReferenceSOAttachmentPayloadSchema >; -export type ExternalReferenceNoSOAttachmentPayload = rt.TypeOf< - typeof ExternalReferenceNoSOAttachmentPayloadRt +export type ExternalReferenceNoSOAttachmentPayload = z.infer< + typeof ExternalReferenceNoSOAttachmentPayloadSchema >; - -export type ExternalReferenceAttachmentAttributes = rt.TypeOf< - typeof ExternalReferenceAttachmentAttributesRt +export type ExternalReferenceAttachmentAttributes = z.infer< + typeof ExternalReferenceAttachmentAttributesSchema >; - -export type ExternalReferenceSOAttachmentAttributes = rt.TypeOf< - typeof ExternalReferenceSOAttachmentAttributesRt +export type ExternalReferenceSOAttachmentAttributes = z.infer< + typeof ExternalReferenceSOAttachmentAttributesSchema >; - -export type ExternalReferenceNoSOAttachmentAttributes = rt.TypeOf< - typeof ExternalReferenceNoSOAttachmentAttributesRt +export type ExternalReferenceNoSOAttachmentAttributes = z.infer< + typeof ExternalReferenceNoSOAttachmentAttributesSchema >; - -export type ExternalReferenceWithoutRefsAttachmentPayload = rt.TypeOf< - typeof ExternalReferenceWithoutRefsAttachmentPayloadRt +export type ExternalReferenceWithoutRefsAttachmentPayload = z.infer< + typeof ExternalReferenceWithoutRefsAttachmentPayloadSchema >; -export type ExternalReferenceAttachment = rt.TypeOf; +export type ExternalReferenceAttachment = z.infer; /** * Persistable state */ - -export const PersistableStateAttachmentPayloadRt = rt.strict({ - type: rt.literal(AttachmentType.persistableState), - owner: rt.string, - persistableStateAttachmentTypeId: rt.string, - persistableStateAttachmentState: rt.record(rt.string, jsonValueRt), +export const PersistableStateAttachmentPayloadSchema = z.object({ + type: z.literal(AttachmentType.persistableState), + owner: z.string(), + persistableStateAttachmentTypeId: z.string(), + persistableStateAttachmentState: z.record(z.string(), jsonValueSchema), }); -const PersistableStateAttachmentAttributesRt = rt.intersection([ - PersistableStateAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +const PersistableStateAttachmentAttributesSchema = PersistableStateAttachmentPayloadSchema.merge( + AttachmentAttributesBasicSchema +); -export const PersistableStateAttachmentRt = rt.intersection([ - PersistableStateAttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const PersistableStateAttachmentSchema = PersistableStateAttachmentAttributesSchema.extend({ + id: z.string(), + version: z.string(), +}); -export type PersistableStateAttachmentPayload = rt.TypeOf< - typeof PersistableStateAttachmentPayloadRt +export type PersistableStateAttachmentPayload = z.infer< + typeof PersistableStateAttachmentPayloadSchema >; -export type PersistableStateAttachment = rt.TypeOf; -export type PersistableStateAttachmentAttributes = rt.TypeOf< - typeof PersistableStateAttachmentAttributesRt +export type PersistableStateAttachment = z.infer; +export type PersistableStateAttachmentAttributes = z.infer< + typeof PersistableStateAttachmentAttributesSchema >; /** * Common */ - -export const AttachmentPayloadRt = rt.union([ - UserCommentAttachmentPayloadRt, - AlertAttachmentPayloadRt, - EventAttachmentPayloadRt, - ActionsAttachmentPayloadRt, - ExternalReferenceNoSOAttachmentPayloadRt, - ExternalReferenceSOAttachmentPayloadRt, - PersistableStateAttachmentPayloadRt, +export const AttachmentPayloadSchema = z.union([ + UserCommentAttachmentPayloadSchema, + AlertAttachmentPayloadSchema, + EventAttachmentPayloadSchema, + ActionsAttachmentPayloadSchema, + ExternalReferenceNoSOAttachmentPayloadSchema, + ExternalReferenceSOAttachmentPayloadSchema, + PersistableStateAttachmentPayloadSchema, ]); -export const AttachmentAttributesRt = rt.union([ - UserCommentAttachmentAttributesRt, - AlertAttachmentAttributesRt, - EventAttachmentAttributesRt, - ActionsAttachmentAttributesRt, - ExternalReferenceAttachmentAttributesRt, - PersistableStateAttachmentAttributesRt, +export const AttachmentAttributesSchema = z.union([ + UserCommentAttachmentAttributesSchema, + AlertAttachmentAttributesSchema, + EventAttachmentAttributesSchema, + ActionsAttachmentAttributesSchema, + ExternalReferenceAttachmentAttributesSchema, + PersistableStateAttachmentAttributesSchema, ]); -const AttachmentAttributesNoSORt = rt.union([ - UserCommentAttachmentAttributesRt, - AlertAttachmentAttributesRt, - EventAttachmentAttributesRt, - ActionsAttachmentAttributesRt, - ExternalReferenceNoSOAttachmentAttributesRt, - PersistableStateAttachmentAttributesRt, +const AttachmentAttributesNoSOSchema = z.union([ + UserCommentAttachmentAttributesSchema, + AlertAttachmentAttributesSchema, + EventAttachmentAttributesSchema, + ActionsAttachmentAttributesSchema, + ExternalReferenceNoSOAttachmentAttributesSchema, + PersistableStateAttachmentAttributesSchema, ]); -const AttachmentAttributesWithoutRefsRt = rt.union([ - UserCommentAttachmentAttributesRt, - AlertAttachmentAttributesRt, - EventAttachmentAttributesRt, - ActionsAttachmentAttributesRt, - ExternalReferenceWithoutRefsAttachmentAttributesRt, - PersistableStateAttachmentAttributesRt, +const AttachmentAttributesWithoutRefsSchema = z.union([ + UserCommentAttachmentAttributesSchema, + AlertAttachmentAttributesSchema, + EventAttachmentAttributesSchema, + ActionsAttachmentAttributesSchema, + ExternalReferenceWithoutRefsAttachmentAttributesSchema, + PersistableStateAttachmentAttributesSchema, ]); -export const AttachmentRt = rt.intersection([ - AttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const AttachmentSchema = AttachmentAttributesSchema.and( + z.object({ id: z.string(), version: z.string() }) +); -export const AttachmentsRt = rt.array(AttachmentRt); +export const AttachmentsSchema = z.array(AttachmentSchema); /** * This type is used by the CaseService. @@ -383,22 +334,21 @@ export const AttachmentsRt = rt.array(AttachmentRt); * we need to make all of our attributes partial too. * We ensure that partial updates of CommentContext is not going to happen inside the patch comment route. */ -export const AttachmentPatchAttributesRt = rt.intersection([ - rt.union([ - rt.exact(rt.partial(UserCommentAttachmentPayloadRt.type.props)), - rt.exact(rt.partial(AlertAttachmentPayloadRt.type.props)), - rt.exact(rt.partial(EventAttachmentPayloadRt.type.props)), - rt.exact(rt.partial(ActionsAttachmentPayloadRt.type.props)), - rt.exact(rt.partial(ExternalReferenceNoSOAttachmentPayloadRt.type.props)), - rt.exact(rt.partial(ExternalReferenceSOAttachmentPayloadRt.type.props)), - rt.exact(rt.partial(PersistableStateAttachmentPayloadRt.type.props)), - ]), - rt.exact(rt.partial(AttachmentAttributesBasicRt.type.props)), -]); - -export type AttachmentAttributes = rt.TypeOf; -export type AttachmentAttributesNoSO = rt.TypeOf; -export type AttachmentAttributesWithoutRefs = rt.TypeOf; -export type AttachmentPatchAttributes = rt.TypeOf; -export type Attachment = rt.TypeOf; -export type Attachments = rt.TypeOf; +export const AttachmentPatchAttributesSchema = z + .union([ + UserCommentAttachmentPayloadSchema.partial(), + AlertAttachmentPayloadSchema.partial(), + EventAttachmentPayloadSchema.partial(), + ActionsAttachmentPayloadSchema.partial(), + ExternalReferenceNoSOAttachmentPayloadSchema.partial(), + ExternalReferenceSOAttachmentPayloadSchema.partial(), + PersistableStateAttachmentPayloadSchema.partial(), + ]) + .and(AttachmentAttributesBasicSchema.partial()); + +export type AttachmentAttributes = z.infer; +export type AttachmentAttributesNoSO = z.infer; +export type AttachmentAttributesWithoutRefs = z.infer; +export type AttachmentPatchAttributes = z.infer; +export type Attachment = z.infer; +export type Attachments = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.test.ts index a2ad571cd54dd..ce84a8b76ca57 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.test.ts @@ -6,17 +6,17 @@ */ import { - UnifiedAttachmentPayloadRt, - UnifiedAttachmentAttributesRt, - UnifiedAttachmentRt, - AttachmentRtV2, - DocumentAttachmentAttributesRtV2, + UnifiedAttachmentPayloadSchema, + UnifiedAttachmentAttributesSchema, + UnifiedAttachmentSchema, + AttachmentSchemaV2, + DocumentAttachmentAttributesSchemaV2, } from './v2'; import { AttachmentType } from './v1'; import { SECURITY_EVENT_ATTACHMENT_TYPE } from '../../../constants/attachments'; describe('Unified Attachments', () => { - describe('UnifiedAttachmentPayloadRt', () => { + describe('UnifiedAttachmentPayloadSchema', () => { const defaultRequest = { type: 'lens', attachmentId: 'attachment-123', @@ -37,21 +37,15 @@ describe('Unified Attachments', () => { }; it('has expected attributes in request', () => { - const query = UnifiedAttachmentPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = UnifiedAttachmentPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = UnifiedAttachmentPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = UnifiedAttachmentPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts null data', () => { @@ -63,12 +57,9 @@ describe('Unified Attachments', () => { metadata: null, }; - const query = UnifiedAttachmentPayloadRt.decode(requestWithNullData); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithNullData, - }); + const result = UnifiedAttachmentPayloadSchema.safeParse(requestWithNullData); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithNullData); }); it('accepts request with only data', () => { @@ -80,11 +71,9 @@ describe('Unified Attachments', () => { }, }; - const query = UnifiedAttachmentPayloadRt.decode(requestWithoutAttachmentId); - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithoutAttachmentId, - }); + const result = UnifiedAttachmentPayloadSchema.safeParse(requestWithoutAttachmentId); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithoutAttachmentId); }); it('accepts request with only attachmentId', () => { @@ -94,12 +83,11 @@ describe('Unified Attachments', () => { owner: 'securitySolution', }; - const query = UnifiedAttachmentPayloadRt.decode(requestWithOnlyAttachmentId); - - expect(query._tag).toBe('Right'); - if (query._tag === 'Right') { - expect(query.right).toMatchObject(requestWithOnlyAttachmentId); - expect(query.right).not.toHaveProperty('metadata'); + const result = UnifiedAttachmentPayloadSchema.safeParse(requestWithOnlyAttachmentId); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toMatchObject(requestWithOnlyAttachmentId); + expect(result.data).not.toHaveProperty('metadata'); } }); @@ -110,11 +98,10 @@ describe('Unified Attachments', () => { owner: 'securitySolution', }; - const query = UnifiedAttachmentPayloadRt.decode(requestWithAttachmentIdArray); - - expect(query._tag).toBe('Right'); - if (query._tag === 'Right') { - expect(query.right).toMatchObject(requestWithAttachmentIdArray); + const result = UnifiedAttachmentPayloadSchema.safeParse(requestWithAttachmentIdArray); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toMatchObject(requestWithAttachmentIdArray); } }); @@ -128,13 +115,11 @@ describe('Unified Attachments', () => { }, }; - const query = UnifiedAttachmentPayloadRt.decode(requestWithAttachmentIdAndMetadata); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithAttachmentIdAndMetadata, - }); + const result = UnifiedAttachmentPayloadSchema.safeParse(requestWithAttachmentIdAndMetadata); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithAttachmentIdAndMetadata); }); + it('accepts request without metadata', () => { const requestWithoutMetadata = { type: 'lens', @@ -147,11 +132,10 @@ describe('Unified Attachments', () => { }, }; - const query = UnifiedAttachmentPayloadRt.decode(requestWithoutMetadata); - - expect(query._tag).toBe('Right'); - if (query._tag === 'Right') { - expect(query.right).toMatchObject({ + const result = UnifiedAttachmentPayloadSchema.safeParse(requestWithoutMetadata); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toMatchObject({ type: 'lens', attachmentId: 'attachment-123', owner: 'securitySolution', @@ -161,8 +145,7 @@ describe('Unified Attachments', () => { }, }, }); - // metadata should not be present when not provided - expect(query.right).not.toHaveProperty('metadata'); + expect(result.data).not.toHaveProperty('metadata'); } }); @@ -172,13 +155,12 @@ describe('Unified Attachments', () => { owner: 'securitySolution', }; - const query = UnifiedAttachmentPayloadRt.decode(requestWithOnlyType); - - expect(query._tag).toBe('Left'); + const result = UnifiedAttachmentPayloadSchema.safeParse(requestWithOnlyType); + expect(result.success).toBe(false); }); }); - describe('UnifiedAttachmentAttributesRt', () => { + describe('UnifiedAttachmentAttributesSchema', () => { const defaultRequest = { type: 'lens', attachmentId: 'attachment-123', @@ -204,21 +186,15 @@ describe('Unified Attachments', () => { }; it('has expected attributes in request', () => { - const query = UnifiedAttachmentAttributesRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = UnifiedAttachmentAttributesSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = UnifiedAttachmentAttributesRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = UnifiedAttachmentAttributesSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts request with only attachmentId', () => { @@ -238,12 +214,9 @@ describe('Unified Attachments', () => { pushed_by: null, }; - const query = UnifiedAttachmentAttributesRt.decode(requestWithOnlyAttachmentId); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithOnlyAttachmentId, - }); + const result = UnifiedAttachmentAttributesSchema.safeParse(requestWithOnlyAttachmentId); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithOnlyAttachmentId); }); it('accepts request with only data', () => { @@ -265,12 +238,9 @@ describe('Unified Attachments', () => { pushed_by: null, }; - const query = UnifiedAttachmentAttributesRt.decode(requestWithOnlyData); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithOnlyData, - }); + const result = UnifiedAttachmentAttributesSchema.safeParse(requestWithOnlyData); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithOnlyData); }); it('rejects request with neither attachmentId nor data', () => { @@ -289,13 +259,12 @@ describe('Unified Attachments', () => { pushed_by: null, }; - const query = UnifiedAttachmentAttributesRt.decode(requestWithoutRequired); - - expect(query._tag).toBe('Left'); + const result = UnifiedAttachmentAttributesSchema.safeParse(requestWithoutRequired); + expect(result.success).toBe(false); }); }); - describe('UnifiedAttachmentRt', () => { + describe('UnifiedAttachmentSchema', () => { const defaultRequest = { type: 'lens', attachmentId: 'attachment-123', @@ -323,21 +292,15 @@ describe('Unified Attachments', () => { }; it('has expected attributes in request', () => { - const query = UnifiedAttachmentRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = UnifiedAttachmentSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = UnifiedAttachmentRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = UnifiedAttachmentSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts request with only attachmentId', () => { @@ -359,12 +322,9 @@ describe('Unified Attachments', () => { version: 'WzEwMCwxXQ==', }; - const query = UnifiedAttachmentRt.decode(requestWithOnlyAttachmentId); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithOnlyAttachmentId, - }); + const result = UnifiedAttachmentSchema.safeParse(requestWithOnlyAttachmentId); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithOnlyAttachmentId); }); it('accepts request with only data', () => { @@ -388,12 +348,9 @@ describe('Unified Attachments', () => { version: 'WzEwMCwxXQ==', }; - const query = UnifiedAttachmentRt.decode(requestWithOnlyData); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithOnlyData, - }); + const result = UnifiedAttachmentSchema.safeParse(requestWithOnlyData); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithOnlyData); }); it('rejects request with neither attachmentId nor data', () => { @@ -414,14 +371,13 @@ describe('Unified Attachments', () => { version: 'WzEwMCwxXQ==', }; - const query = UnifiedAttachmentRt.decode(requestWithoutRequired); - - expect(query._tag).toBe('Left'); + const result = UnifiedAttachmentSchema.safeParse(requestWithoutRequired); + expect(result.success).toBe(false); }); }); - describe('AttachmentRtV2', () => { - it('accepts UnifiedAttachmentRt', () => { + describe('AttachmentSchemaV2', () => { + it('accepts UnifiedAttachmentSchema', () => { const unifiedAttachment = { type: 'lens', attachmentId: 'attachment-123', @@ -440,15 +396,12 @@ describe('Unified Attachments', () => { version: 'WzEwMCwxXQ==', }; - const query = AttachmentRtV2.decode(unifiedAttachment); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: unifiedAttachment, - }); + const result = AttachmentSchemaV2.safeParse(unifiedAttachment); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(unifiedAttachment); }); - it('accepts AttachmentRt (v1)', () => { + it('accepts AttachmentSchema (v1)', () => { const v1Attachment = { type: AttachmentType.user, comment: 'This is a comment', @@ -467,16 +420,13 @@ describe('Unified Attachments', () => { version: 'WzEwMCwxXQ==', }; - const query = AttachmentRtV2.decode(v1Attachment); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: v1Attachment, - }); + const result = AttachmentSchemaV2.safeParse(v1Attachment); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(v1Attachment); }); }); - describe('DocumentAttachmentAttributesRtV2', () => { + describe('DocumentAttachmentAttributesSchemaV2', () => { it('accepts legacy event attributes', () => { const legacyEvent = { type: AttachmentType.event, @@ -491,7 +441,8 @@ describe('Unified Attachments', () => { pushed_by: null, }; - expect(DocumentAttachmentAttributesRtV2.decode(legacyEvent)._tag).toBe('Right'); + const result = DocumentAttachmentAttributesSchemaV2.safeParse(legacyEvent); + expect(result.success).toBe(true); }); it('accepts unified security.event attributes', () => { @@ -508,7 +459,8 @@ describe('Unified Attachments', () => { pushed_by: null, }; - expect(DocumentAttachmentAttributesRtV2.decode(unifiedEvent)._tag).toBe('Right'); + const result = DocumentAttachmentAttributesSchemaV2.safeParse(unifiedEvent); + expect(result.success).toBe(true); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.ts index 93c42e9349de9..ca6b0f019382b 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/attachment/v2.ts @@ -5,16 +5,16 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { jsonValueRt } from '../../../api'; +import { z } from '@kbn/zod/v4'; +import { jsonValueSchema } from '../../../schema'; import { SECURITY_EVENT_ATTACHMENT_TYPE } from '../../../constants/attachments'; import { - AlertAttachmentAttributesRt, - EventAttachmentAttributesRt, - AttachmentAttributesBasicRt, - AttachmentAttributesRt, - AttachmentPatchAttributesRt, - AttachmentRt, + AlertAttachmentAttributesSchema, + AttachmentAttributesBasicSchema, + AttachmentAttributesSchema, + AttachmentPatchAttributesSchema, + AttachmentSchema, + EventAttachmentAttributesSchema, } from './v1'; /** @@ -24,19 +24,13 @@ import { * - metadata: optional - additional metadata about the reference * - data: optional - some reference attachments may also have data */ -export const UnifiedReferenceAttachmentPayloadRt = rt.intersection([ - rt.strict({ - type: rt.string, - attachmentId: rt.union([rt.string, rt.array(rt.string)]), - owner: rt.string, - }), - rt.exact( - rt.partial({ - data: rt.union([rt.null, rt.record(rt.string, jsonValueRt)]), - metadata: rt.union([rt.null, rt.record(rt.string, jsonValueRt)]), - }) - ), -]); +export const UnifiedReferenceAttachmentPayloadSchema = z.object({ + type: z.string(), + attachmentId: z.union([z.string(), z.array(z.string())]), + owner: z.string(), + data: z.record(z.string(), jsonValueSchema).nullable().optional(), + metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), +}); /** * Payload for Value-based Attachments @@ -44,44 +38,33 @@ export const UnifiedReferenceAttachmentPayloadRt = rt.intersection([ * - data: required - contains content/state (user comments, persistable state, visualizations) * - metadata: optional - additional metadata */ -export const UnifiedValueAttachmentPayloadRt = rt.intersection([ - rt.strict({ - type: rt.string, - data: rt.record(rt.string, jsonValueRt), - owner: rt.string, - }), - rt.exact( - rt.partial({ - metadata: rt.union([rt.null, rt.record(rt.string, jsonValueRt)]), - }) - ), -]); +export const UnifiedValueAttachmentPayloadSchema = z.object({ + type: z.string(), + data: z.record(z.string(), jsonValueSchema), + owner: z.string(), + metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), +}); -export const UnifiedAttachmentPayloadRt = rt.union([ - UnifiedReferenceAttachmentPayloadRt, - UnifiedValueAttachmentPayloadRt, +export const UnifiedAttachmentPayloadSchema = z.union([ + UnifiedReferenceAttachmentPayloadSchema, + UnifiedValueAttachmentPayloadSchema, ]); /** * Saved Object attributes for Unified Attachments * Contains the payload and the basic attributes */ -export const UnifiedAttachmentAttributesRt = rt.intersection([ - UnifiedAttachmentPayloadRt, - AttachmentAttributesBasicRt, -]); +export const UnifiedAttachmentAttributesSchema = UnifiedAttachmentPayloadSchema.and( + AttachmentAttributesBasicSchema +); /** * Full Saved Object representationfor Unified Attachments * Contains payload, basic attributes and id and version */ -export const UnifiedAttachmentRt = rt.intersection([ - UnifiedAttachmentAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +export const UnifiedAttachmentSchema = UnifiedAttachmentAttributesSchema.and( + z.object({ id: z.string(), version: z.string() }) +); /** * Partial payload props for patch (reference and value). We define these explicitly because @@ -89,90 +72,88 @@ export const UnifiedAttachmentRt = rt.intersection([ * so they have no .type.props (only rt.strict() codecs have .props); v1 payloads use rt.strict() * so AttachmentPatchAttributesRt can use .type.props there. */ -const UnifiedReferenceAttachmentPayloadPartialRt = rt.exact( - rt.partial({ - type: rt.string, - owner: rt.string, - attachmentId: rt.union([rt.string, rt.array(rt.string)]), - data: rt.union([rt.null, rt.record(rt.string, jsonValueRt)]), - metadata: rt.union([rt.null, rt.record(rt.string, jsonValueRt)]), - }) -); -const UnifiedValueAttachmentPayloadPartialRt = rt.exact( - rt.partial({ - type: rt.string, - owner: rt.string, - data: rt.record(rt.string, jsonValueRt), - metadata: rt.union([rt.null, rt.record(rt.string, jsonValueRt)]), - }) -); +const UnifiedReferenceAttachmentPayloadPartialSchema = z.object({ + type: z.string().optional(), + attachmentId: z.union([z.string(), z.array(z.string())]).optional(), + data: z.record(z.string(), jsonValueSchema).nullable().optional(), + metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), +}); -export const UnifiedAttachmentPatchAttributesRt = rt.intersection([ - rt.union([UnifiedReferenceAttachmentPayloadPartialRt, UnifiedValueAttachmentPayloadPartialRt]), - rt.exact(rt.partial(AttachmentAttributesBasicRt.type.props)), -]); +const UnifiedValueAttachmentPayloadPartialSchema = z.object({ + type: z.string().optional(), + data: z.record(z.string(), jsonValueSchema).optional(), + metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), +}); -export type UnifiedReferenceAttachmentPayload = rt.TypeOf< - typeof UnifiedReferenceAttachmentPayloadRt +export const UnifiedAttachmentPatchAttributesSchema = z + .union([ + UnifiedReferenceAttachmentPayloadPartialSchema, + UnifiedValueAttachmentPayloadPartialSchema, + ]) + .and(AttachmentAttributesBasicSchema.partial()); + +export type UnifiedReferenceAttachmentPayload = z.infer< + typeof UnifiedReferenceAttachmentPayloadSchema >; -export type UnifiedValueAttachmentPayload = rt.TypeOf; -export type UnifiedAttachmentPayload = rt.TypeOf; -export type UnifiedAttachmentAttributes = rt.TypeOf; -export type UnifiedAttachment = rt.TypeOf; - -const UnifiedEventDocumentAttachmentMetadataRt = rt.union([ - rt.null, - rt.exact( - rt.partial({ - index: rt.union([rt.string, rt.array(rt.string)]), - }) - ), -]); +export type UnifiedValueAttachmentPayload = z.infer; +export type UnifiedAttachmentPayload = z.infer; +export type UnifiedAttachmentAttributes = z.infer; +export type UnifiedAttachment = z.infer; -const UnifiedEventDocumentAttachmentPayloadRt = rt.intersection([ - rt.strict({ - type: rt.literal(SECURITY_EVENT_ATTACHMENT_TYPE), - attachmentId: rt.union([rt.string, rt.array(rt.string)]), - owner: rt.string, - }), - rt.exact( - rt.partial({ - metadata: UnifiedEventDocumentAttachmentMetadataRt, - }) - ), +/** + * Combined v1 legacy and v2 unified attachment types + */ +export const AttachmentSchemaV2 = z.union([AttachmentSchema, UnifiedAttachmentSchema]); +export const AttachmentsSchemaV2 = z.array(AttachmentSchemaV2); +export const AttachmentAttributesSchemaV2 = z.union([ + AttachmentAttributesSchema, + UnifiedAttachmentAttributesSchema, ]); - -const UnifiedEventDocumentAttachmentAttributesRt = rt.intersection([ - UnifiedEventDocumentAttachmentPayloadRt, - AttachmentAttributesBasicRt, +export const AttachmentPatchAttributesSchemaV2 = z.union([ + AttachmentPatchAttributesSchema, + UnifiedAttachmentPatchAttributesSchema, ]); -export const DocumentAttachmentAttributesRtV2 = rt.union([ - AlertAttachmentAttributesRt, - EventAttachmentAttributesRt, - UnifiedEventDocumentAttachmentAttributesRt, +const UnifiedEventDocumentAttachmentMetadataSchema = z + .union([ + z.null(), + z + .object({ + index: z.union([z.string(), z.array(z.string())]), + }) + .partial(), + ]) + .optional(); + +const UnifiedEventDocumentAttachmentPayloadSchema = z + .object({ + type: z.literal(SECURITY_EVENT_ATTACHMENT_TYPE), + attachmentId: z.union([z.string(), z.array(z.string())]), + owner: z.string(), + }) + .and( + z + .object({ + metadata: UnifiedEventDocumentAttachmentMetadataSchema, + }) + .partial() + ); + +const UnifiedEventDocumentAttachmentAttributesSchema = + UnifiedEventDocumentAttachmentPayloadSchema.and(AttachmentAttributesBasicSchema); + +export const DocumentAttachmentAttributesSchemaV2 = z.union([ + AlertAttachmentAttributesSchema, + EventAttachmentAttributesSchema, + UnifiedEventDocumentAttachmentAttributesSchema, ]); -export type DocumentAttachmentAttributesV2 = rt.TypeOf; +export type AttachmentV2 = z.infer; +export type AttachmentsV2 = z.infer; +export type AttachmentAttributesV2 = z.infer; +export type AttachmentPatchAttributesV2 = z.infer; +export type DocumentAttachmentAttributesV2 = z.infer; /** * Transitional read-shape mode while v1/v2 attachments coexist. */ export type AttachmentMode = 'legacy' | 'unified'; - -/** - * Combined v1 legacy and v2 unified attachment types - */ -export const AttachmentRtV2 = rt.union([AttachmentRt, UnifiedAttachmentRt]); -export const AttachmentsRtV2 = rt.array(AttachmentRtV2); -export const AttachmentAttributesRtV2 = rt.union([ - AttachmentAttributesRt, - UnifiedAttachmentAttributesRt, -]); -export const AttachmentPatchAttributesRtV2 = rt.union([ - AttachmentPatchAttributesRt, - UnifiedAttachmentPatchAttributesRt, -]); -export type AttachmentV2 = rt.TypeOf; -export type AttachmentsV2 = rt.TypeOf; -export type AttachmentAttributesV2 = rt.TypeOf; -export type AttachmentPatchAttributesV2 = rt.TypeOf; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.test.ts index 346a7c1ebe8a9..119c848a7c238 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.test.ts @@ -5,110 +5,18 @@ * 2.0. */ -import { AttachmentType } from '../attachment/v1'; import { ConnectorTypes } from '../connector/v1'; import { CASE_EXTENDED_FIELDS } from '../../../constants'; import { CaseAttributesSchema, CaseSettingsSchema, + CasesSchema, RelatedCaseSchema, -} from '../../domain_zod/case/v1'; -import { - CaseAttributesRt, - CaseSettingsRt, CaseSeverity, - CasesRt, CaseStatuses, - RelatedCaseRt, } from './v1'; -const basicCase = { - owner: 'cases', - closed_at: null, - closed_by: null, - id: 'basic-case-id', - comments: [ - { - comment: 'Solve this fast!', - type: AttachmentType.user, - id: 'basic-comment-id', - created_at: '2020-02-19T23:06:33.798Z', - created_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, - owner: 'cases', - pushed_at: null, - pushed_by: null, - updated_at: null, - updated_by: null, - version: 'WzQ3LDFc', - }, - ], - created_at: '2020-02-19T23:06:33.798Z', - created_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, - connector: { - id: 'none', - name: 'My Connector', - type: ConnectorTypes.none, - fields: null, - }, - description: 'Security banana Issue', - severity: CaseSeverity.LOW, - duration: null, - external_service: null, - status: CaseStatuses.open, - tags: ['coke', 'pepsi'], - title: 'Another horrible breach!!', - totalComment: 1, - totalAlerts: 0, - totalEvents: 0, - updated_at: '2020-02-20T15:02:57.995Z', - updated_by: { - full_name: 'Leslie Knope', - username: 'lknope', - email: 'leslie.knope@elastic.co', - }, - version: 'WzQ3LDFd', - settings: { - syncAlerts: true, - extractObservables: false, - }, - // damaged_raccoon uid - assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], - category: null, - customFields: [ - { - key: 'first_custom_field_key', - type: 'text', - value: 'this is a text field value', - }, - { - key: 'second_custom_field_key', - type: 'toggle', - value: true, - }, - { - key: 'third_custom_field_key', - type: 'number', - value: 0, - }, - ], - observables: [], - total_observables: 0, - incremental_id: undefined, - in_progress_at: undefined, - time_to_acknowledge: undefined, - time_to_investigate: undefined, - time_to_resolve: undefined, -}; - -describe('RelatedCaseRt', () => { +describe('RelatedCaseSchema', () => { const defaultRequest = { id: 'basic-case-id', title: 'basic-case-title', @@ -121,79 +29,37 @@ describe('RelatedCaseRt', () => { events: 0, }, }; - it('has expected attributes in request', () => { - const query = RelatedCaseRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = RelatedCaseRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from totals', () => { - const query = RelatedCaseRt.decode({ - ...defaultRequest, - totals: { ...defaultRequest.totals, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - it('zod: has expected attributes in request', () => { + it('has expected attributes in request', () => { const result = RelatedCaseSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = RelatedCaseSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); -}); - -describe('SettingsRt', () => { - it('has expected attributes in request', () => { - const query = CaseSettingsRt.decode({ syncAlerts: true, extractObservables: true }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { syncAlerts: true, extractObservables: true }, - }); - }); - it('removes foo:bar attributes from request', () => { - const query = CaseSettingsRt.decode({ - syncAlerts: false, - extractObservables: false, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { syncAlerts: false, extractObservables: false }, + it('strips unknown fields from totals', () => { + const result = RelatedCaseSchema.safeParse({ + ...defaultRequest, + totals: { ...defaultRequest.totals, foo: 'bar' }, }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); +}); - it('zod: has expected attributes in request', () => { +describe('CaseSettingsSchema', () => { + it('has expected attributes in request', () => { const result = CaseSettingsSchema.safeParse({ syncAlerts: true, extractObservables: true }); expect(result.success).toBe(true); expect(result.data).toStrictEqual({ syncAlerts: true, extractObservables: true }); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CaseSettingsSchema.safeParse({ syncAlerts: false, extractObservables: false, @@ -204,7 +70,7 @@ describe('SettingsRt', () => { }); }); -describe('CaseAttributesRt', () => { +describe('CaseAttributesSchema', () => { const defaultRequest = { description: 'A description', status: CaseStatuses.open, @@ -255,230 +121,108 @@ describe('CaseAttributesRt', () => { ], observables: [], total_observables: 0, - in_progress_at: undefined, - time_to_acknowledge: undefined, - time_to_investigate: undefined, - time_to_resolve: undefined, }; it('has expected attributes in request', () => { - const query = CaseAttributesRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CaseAttributesSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CaseAttributesRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CaseAttributesSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from connector', () => { - const query = CaseAttributesRt.decode({ + it('strips unknown fields from connector', () => { + const result = CaseAttributesSchema.safeParse({ ...defaultRequest, connector: { ...defaultRequest.connector, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from created_by', () => { - const query = CaseAttributesRt.decode({ + it('strips unknown fields from created_by', () => { + const result = CaseAttributesSchema.safeParse({ ...defaultRequest, created_by: { ...defaultRequest.created_by, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts optional template and extended_fields', () => { - const request = { + const zodRequest = { ...defaultRequest, template: { id: 'template-id', version: 1 }, [CASE_EXTENDED_FIELDS]: { field1: 'foo' }, }; - - const query = CaseAttributesRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: request, - }); - }); - - it('removes unknown attributes from template', () => { - const request = { - ...defaultRequest, - template: { id: 'template-id', version: 1, foo: 'bar' }, - }; - - const query = CaseAttributesRt.decode(request); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - template: { id: 'template-id', version: 1 }, - }, - }); - }); - - it('zod: has expected attributes in request', () => { - const zodRequest = { - description: defaultRequest.description, - status: defaultRequest.status, - tags: defaultRequest.tags, - title: defaultRequest.title, - connector: defaultRequest.connector, - settings: defaultRequest.settings, - owner: defaultRequest.owner, - severity: defaultRequest.severity, - assignees: defaultRequest.assignees, - duration: defaultRequest.duration, - closed_at: defaultRequest.closed_at, - closed_by: defaultRequest.closed_by, - created_at: defaultRequest.created_at, - created_by: defaultRequest.created_by, - external_service: defaultRequest.external_service, - updated_at: defaultRequest.updated_at, - updated_by: defaultRequest.updated_by, - category: defaultRequest.category, - customFields: defaultRequest.customFields, - observables: defaultRequest.observables, - total_observables: defaultRequest.total_observables, - }; const result = CaseAttributesSchema.safeParse(zodRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(zodRequest); }); - it('zod: strips unknown fields', () => { - const zodRequest = { - description: defaultRequest.description, - status: defaultRequest.status, - tags: defaultRequest.tags, - title: defaultRequest.title, - connector: defaultRequest.connector, - settings: defaultRequest.settings, - owner: defaultRequest.owner, - severity: defaultRequest.severity, - assignees: defaultRequest.assignees, - duration: defaultRequest.duration, - closed_at: defaultRequest.closed_at, - closed_by: defaultRequest.closed_by, - created_at: defaultRequest.created_at, - created_by: defaultRequest.created_by, - external_service: defaultRequest.external_service, - updated_at: defaultRequest.updated_at, - updated_by: defaultRequest.updated_by, - category: defaultRequest.category, - customFields: defaultRequest.customFields, - observables: defaultRequest.observables, - total_observables: defaultRequest.total_observables, - }; - const result = CaseAttributesSchema.safeParse({ ...zodRequest, foo: 'bar' }); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(zodRequest); - }); - - it('zod: strips unknown fields from connector', () => { - const zodRequest = { - description: defaultRequest.description, - status: defaultRequest.status, - tags: defaultRequest.tags, - title: defaultRequest.title, - connector: defaultRequest.connector, - settings: defaultRequest.settings, - owner: defaultRequest.owner, - severity: defaultRequest.severity, - assignees: defaultRequest.assignees, - duration: defaultRequest.duration, - closed_at: defaultRequest.closed_at, - closed_by: defaultRequest.closed_by, - created_at: defaultRequest.created_at, - created_by: defaultRequest.created_by, - external_service: defaultRequest.external_service, - updated_at: defaultRequest.updated_at, - updated_by: defaultRequest.updated_by, - category: defaultRequest.category, - customFields: defaultRequest.customFields, - observables: defaultRequest.observables, - total_observables: defaultRequest.total_observables, - }; + it('strips unknown fields from template', () => { const result = CaseAttributesSchema.safeParse({ - ...zodRequest, - connector: { ...zodRequest.connector, foo: 'bar' }, + ...defaultRequest, + template: { id: 'template-id', version: 1, foo: 'bar' }, + [CASE_EXTENDED_FIELDS]: { field1: 'foo' }, }); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(zodRequest); - }); - - it('zod: accepts optional template and extended_fields', () => { - const zodRequest = { - description: defaultRequest.description, - status: defaultRequest.status, - tags: defaultRequest.tags, - title: defaultRequest.title, - connector: defaultRequest.connector, - settings: defaultRequest.settings, - owner: defaultRequest.owner, - severity: defaultRequest.severity, - assignees: defaultRequest.assignees, - duration: defaultRequest.duration, - closed_at: defaultRequest.closed_at, - closed_by: defaultRequest.closed_by, - created_at: defaultRequest.created_at, - created_by: defaultRequest.created_by, - external_service: defaultRequest.external_service, - updated_at: defaultRequest.updated_at, - updated_by: defaultRequest.updated_by, - category: defaultRequest.category, - customFields: defaultRequest.customFields, - observables: defaultRequest.observables, - total_observables: defaultRequest.total_observables, + expect(result.data).toStrictEqual({ + ...defaultRequest, template: { id: 'template-id', version: 1 }, [CASE_EXTENDED_FIELDS]: { field1: 'foo' }, - }; - const result = CaseAttributesSchema.safeParse(zodRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(zodRequest); + }); }); }); -describe('CasesRt', () => { - const defaultRequest = [ - { - ...basicCase, +describe('CasesSchema', () => { + const caseItem = { + description: 'A description', + status: CaseStatuses.open, + tags: ['new', 'case'], + title: 'My new case', + connector: { + id: '123', + name: 'My connector', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, }, - ]; + settings: { syncAlerts: true, extractObservables: true }, + owner: 'cases', + severity: CaseSeverity.LOW, + assignees: [{ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0' }], + duration: null, + closed_at: null, + closed_by: null, + created_at: '2020-02-19T23:06:33.798Z', + created_by: { full_name: 'Leslie Knope', username: 'lknope', email: 'leslie.knope@elastic.co' }, + external_service: null, + updated_at: '2020-02-20T15:02:57.995Z', + updated_by: null, + category: null, + customFields: [], + observables: [], + total_observables: 0, + id: 'case-id', + version: 'WzQ3LDFd', + totalComment: 3, + totalAlerts: 1, + }; it('has expected attributes in request', () => { - const query = CasesRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CasesSchema.safeParse([caseItem]); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual([caseItem]); }); - it('removes foo:bar attributes from request', () => { - const query = CasesRt.decode([{ ...defaultRequest[0], foo: 'bar' }]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CasesSchema.safeParse([{ ...caseItem, foo: 'bar' }]); + expect(result.success).toBe(true); + expect(result.data?.[0]).not.toHaveProperty('foo'); + expect(result.data?.[0]).toMatchObject(caseItem); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.ts index 75f0bebdc42ec..3a42096c7a00d 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/case/v1.ts @@ -5,222 +5,197 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { CaseStatuses } from '@kbn/cases-components/src/status/types'; +import { Settings } from '../../../bundled-types.gen'; import { CASE_EXTENDED_FIELDS, CASE_EXTENDED_FIELDS_LABELS } from '../../../constants'; -import { ExternalServiceRt } from '../external_service/v1'; -import { CaseAssigneesRt, UserRt } from '../user/v1'; -import { CaseConnectorRt } from '../connector/v1'; -import { AttachmentRtV2 } from '../attachment/v2'; -import { CaseCustomFieldsRt } from '../custom_field/v1'; -import { CaseObservableRt } from '../observable/v1'; +import { ExternalServiceSchema } from '../external_service/v1'; +import { CaseAssigneesSchema, UserSchema } from '../user/v1'; +import { CaseConnectorSchema } from '../connector/v1'; +import { AttachmentSchemaV2 } from '../attachment/v2'; +import { CaseCustomFieldsSchema } from '../custom_field/v1'; +import { CaseObservableSchema } from '../observable/v1'; +export enum CaseSeverity { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + CRITICAL = 'critical', +} export { CaseStatuses }; /** - * Status + * Status — exposed as a TS-enum-typed schema so consumers using + * `CaseStatuses` (the enum from @kbn/cases-components) get matching + * type identity instead of a literal-union shape. */ -export const CaseStatusRt = rt.union([ - rt.literal(CaseStatuses.open), - rt.literal(CaseStatuses['in-progress']), - rt.literal(CaseStatuses.closed), -]); +export const CaseStatusSchema = z.nativeEnum(CaseStatuses); export const caseStatuses = Object.values(CaseStatuses); -export const DefaultCloseReasonRt = rt.union([ - rt.literal('false_positive'), - rt.literal('duplicate'), - rt.literal('true_positive'), - rt.literal('benign_positive'), - rt.literal('automated_closure'), - rt.literal('other'), +export const DefaultCloseReasonSchema = z.union([ + z.literal('false_positive'), + z.literal('duplicate'), + z.literal('true_positive'), + z.literal('benign_positive'), + z.literal('automated_closure'), + z.literal('other'), ]); /** * Close reason */ -export const CaseCloseReasonRt = rt.union([DefaultCloseReasonRt, rt.string]); +export const CaseCloseReasonSchema = z.union([DefaultCloseReasonSchema, z.string()]); /** - * Severity + * Severity — exposed as a TS-enum-typed schema so consumers using + * `CaseSeverity` get matching type identity instead of a literal union. */ - -export enum CaseSeverity { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - CRITICAL = 'critical', -} - -export const CaseSeverityRt = rt.union([ - rt.literal(CaseSeverity.LOW), - rt.literal(CaseSeverity.MEDIUM), - rt.literal(CaseSeverity.HIGH), - rt.literal(CaseSeverity.CRITICAL), -]); +export const CaseSeveritySchema = z.nativeEnum(CaseSeverity); /** * Case */ +export const CaseSettingsSchema = Settings; -export const CaseSettingsRt = rt.intersection([ - rt.strict({ - syncAlerts: rt.boolean, - }), - rt.exact( - rt.partial({ - extractObservables: rt.boolean, - }) - ), -]); - -export const CaseTemplate = rt.strict({ - id: rt.string, - version: rt.number, +export const CaseTemplateSchema = z.object({ + id: z.string(), + version: z.number(), }); const CaseBaseFields = { /** * The description of the case */ - description: rt.string, + description: z.string(), /** * The identifying strings for filter a case */ - tags: rt.array(rt.string), + tags: z.array(z.string()), /** * The title of a case */ - title: rt.string, + title: z.string(), /** * The external system that the case can be synced with */ - connector: CaseConnectorRt, + connector: CaseConnectorSchema, /** * The severity of the case */ - severity: CaseSeverityRt, + severity: CaseSeveritySchema, /** * The users assigned to this case */ - assignees: CaseAssigneesRt, + assignees: CaseAssigneesSchema, /** * The category of the case. */ - category: rt.union([rt.string, rt.null]), + category: z.string().nullable(), /** * An array containing the possible, * user-configured custom fields. */ - customFields: CaseCustomFieldsRt, + customFields: CaseCustomFieldsSchema, /** * The alert sync settings */ - settings: CaseSettingsRt, + settings: CaseSettingsSchema, /** * Observables */ - observables: rt.array(CaseObservableRt), + observables: z.array(CaseObservableSchema), }; -export const CaseBaseOptionalFieldsRt = rt.exact( - rt.partial({ - ...CaseBaseFields, - }) -); +export const CaseBaseOptionalFieldsSchema = z.object({ + description: z.string().optional(), + tags: z.array(z.string()).optional(), + title: z.string().optional(), + connector: CaseConnectorSchema.optional(), + severity: CaseSeveritySchema.optional(), + assignees: CaseAssigneesSchema.optional(), + category: z.string().nullable().optional(), + customFields: CaseCustomFieldsSchema.optional(), + settings: CaseSettingsSchema.optional(), + observables: z.array(CaseObservableSchema).optional(), +}); -const CaseBasicRt = rt.strict({ +const CaseBasicSchema = z.object({ /** * The current status of the case (open, closed, in-progress) */ - status: CaseStatusRt, + status: CaseStatusSchema, /** * The plugin owner of the case */ - owner: rt.string, + owner: z.string(), ...CaseBaseFields, }); -export const CaseAttributesRt = rt.intersection([ - CaseBasicRt, - rt.strict({ - duration: rt.union([rt.number, rt.null]), - closed_at: rt.union([rt.string, rt.null]), - closed_by: rt.union([UserRt, rt.null]), - created_at: rt.string, - created_by: UserRt, - external_service: rt.union([ExternalServiceRt, rt.null]), - updated_at: rt.union([rt.string, rt.null]), - updated_by: rt.union([UserRt, rt.null]), - total_observables: rt.union([rt.number, rt.null]), - }), - rt.exact( - rt.partial({ - incremental_id: rt.union([rt.number, rt.null]), - in_progress_at: rt.union([rt.string, rt.null]), - time_to_acknowledge: rt.union([rt.number, rt.null]), - time_to_investigate: rt.union([rt.number, rt.null]), - time_to_resolve: rt.union([rt.number, rt.null]), - template: rt.union([rt.null, CaseTemplate]), - [CASE_EXTENDED_FIELDS]: rt.record(rt.string, rt.string), - }) - ), -]); +export const CaseAttributesSchema = CaseBasicSchema.extend({ + duration: z.number().nullable(), + closed_at: z.string().nullable(), + closed_by: UserSchema.nullable(), + created_at: z.string(), + created_by: UserSchema, + external_service: ExternalServiceSchema.nullable(), + updated_at: z.string().nullable(), + updated_by: UserSchema.nullable(), + total_observables: z.number().nullable(), + incremental_id: z.number().nullable().optional(), + in_progress_at: z.string().nullable().optional(), + time_to_acknowledge: z.number().nullable().optional(), + time_to_investigate: z.number().nullable().optional(), + time_to_resolve: z.number().nullable().optional(), + template: CaseTemplateSchema.nullable().optional(), + [CASE_EXTENDED_FIELDS]: z.record(z.string(), z.string()).optional(), + // Populated at response time by enrichCasesWithFieldLabels — not persisted to the SO. + // Maps storage keys (e.g. `priority_as_keyword`) to user-facing labels (e.g. "Priority"). + [CASE_EXTENDED_FIELDS_LABELS]: z.record(z.string(), z.string()).optional(), +}); -export const CaseRt = rt.intersection([ - CaseAttributesRt, - rt.strict({ - id: rt.string, - totalComment: rt.number, - totalAlerts: rt.number, - totalEvents: rt.union([rt.number, rt.undefined]), - version: rt.string, - }), - rt.exact( - rt.partial({ - comments: rt.array(AttachmentRtV2), - // Populated at response time by enrichCasesWithFieldLabels — not persisted to the SO. - // Maps storage keys (e.g. `priority_as_keyword`) to user-facing labels (e.g. "Priority"). - [CASE_EXTENDED_FIELDS_LABELS]: rt.record(rt.string, rt.string), - }) - ), -]); +export const CaseSchema = CaseAttributesSchema.extend({ + id: z.string(), + totalComment: z.number(), + totalAlerts: z.number(), + totalEvents: z.number().optional(), + version: z.string(), + comments: z.array(AttachmentSchemaV2).optional(), +}); -export const CasesRt = rt.array(CaseRt); +export const CasesSchema = z.array(CaseSchema); -export const AttachmentTotalsRt = rt.strict({ - alerts: rt.number, - events: rt.number, - userComments: rt.number, +export const AttachmentTotalsSchema = z.object({ + alerts: z.number(), + events: z.number(), + userComments: z.number(), }); -export const RelatedCaseRt = rt.strict({ - id: rt.string, - title: rt.string, - description: rt.string, - status: CaseStatusRt, - createdAt: rt.string, - totals: AttachmentTotalsRt, +export const RelatedCaseSchema = z.object({ + id: z.string(), + title: z.string(), + description: z.string(), + status: CaseStatusSchema, + createdAt: z.string(), + totals: AttachmentTotalsSchema, }); -export const SimilarityRt = rt.strict({ - typeKey: rt.string, - typeLabel: rt.string, - value: rt.string, +export const SimilaritySchema = z.object({ + typeKey: z.string(), + typeLabel: z.string(), + value: z.string(), }); -export const SimilarCaseRt = rt.intersection([ - CaseRt, - rt.strict({ similarities: rt.strict({ observables: rt.array(SimilarityRt) }) }), -]); +export const SimilarCaseSchema = CaseSchema.extend({ + similarities: z.object({ observables: z.array(SimilaritySchema) }), +}); -export type Case = rt.TypeOf; -export type Cases = rt.TypeOf; -export type CaseAttributes = rt.TypeOf; -export type CaseSettings = rt.TypeOf; -export type RelatedCase = rt.TypeOf; -export type AttachmentTotals = rt.TypeOf; -export type CaseBaseOptionalFields = rt.TypeOf; -export type SimilarCase = rt.TypeOf; +export type Case = z.infer; +export type Cases = z.infer; +export type CaseAttributes = z.infer; +export type CaseSettings = z.infer; +export type RelatedCase = z.infer; +export type AttachmentTotals = z.infer; +export type CaseBaseOptionalFields = z.infer; +export type SimilarCase = z.infer; export type SimilarCases = SimilarCase[]; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.test.ts index 5b276d713e2ba..a82710710e655 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.test.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; import { CaseSeverity } from '../case/v1'; import { ConnectorTypes } from '../connector/v1'; +import { CustomFieldTypes } from '../custom_field/v1'; import { ConfigurationAttributesSchema, ConfigurationSchema, @@ -16,16 +16,6 @@ import { TextCustomFieldConfigurationSchema, ToggleCustomFieldConfigurationSchema, NumberCustomFieldConfigurationSchema, -} from '../../domain_zod/configure/v1'; -import { CustomFieldTypes } from '../custom_field/v1'; -import { - ConfigurationAttributesRt, - ConfigurationRt, - CustomFieldConfigurationWithoutTypeRt, - TemplateConfigurationRt, - TextCustomFieldConfigurationRt, - ToggleCustomFieldConfigurationRt, - NumberCustomFieldConfigurationRt, } from './v1'; describe('configure', () => { @@ -112,7 +102,7 @@ describe('configure', () => { caseFields: null, }; - describe('ConfigurationAttributesRt', () => { + describe('ConfigurationAttributesSchema', () => { const defaultRequest = { connector: resilient, closure_type: 'close-by-user', @@ -140,58 +130,19 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = ConfigurationAttributesRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - customFields: [textCustomField, toggleCustomField, numberCustomField], - }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ConfigurationAttributesRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - customFields: [textCustomField, toggleCustomField, numberCustomField], - }, - }); - }); - - it('removes foo:bar attributes from custom fields', () => { - const query = ConfigurationAttributesRt.decode({ - ...defaultRequest, - customFields: [{ ...textCustomField, foo: 'bar' }, toggleCustomField, numberCustomField], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - customFields: [textCustomField, toggleCustomField, numberCustomField], - }, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ConfigurationAttributesSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ConfigurationAttributesSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('ConfigurationRt', () => { + describe('ConfigurationSchema', () => { const defaultRequest = { connector: serviceNow, closure_type: 'close-by-user', @@ -225,49 +176,19 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = ConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from mappings', () => { - const query = ConfigurationRt.decode({ - ...defaultRequest, - mappings: [{ ...defaultRequest.mappings[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ConfigurationSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ConfigurationSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CustomFieldConfigurationWithoutTypeRt', () => { + describe('CustomFieldConfigurationWithoutTypeSchema', () => { const defaultRequest = { key: 'custom_field_key', label: 'Custom field label', @@ -275,30 +196,12 @@ describe('configure', () => { }; it('has expected attributes in request', () => { - const query = CustomFieldConfigurationWithoutTypeRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CustomFieldConfigurationWithoutTypeRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('zod: has expected attributes in request', () => { const result = CustomFieldConfigurationWithoutTypeSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CustomFieldConfigurationWithoutTypeSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -308,7 +211,7 @@ describe('configure', () => { }); }); - describe('TextCustomFieldConfigurationRt', () => { + describe('TextCustomFieldConfigurationSchema', () => { const defaultRequest = { key: 'my_text_custom_field', label: 'Text Custom Field', @@ -316,60 +219,13 @@ describe('configure', () => { required: false, }; - it('has expected attributes in request with required: false', () => { - const query = TextCustomFieldConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('has expected attributes in request with defaultValue and required: true', () => { - const query = TextCustomFieldConfigurationRt.decode({ - ...defaultRequest, - required: true, - defaultValue: 'foobar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - required: true, - defaultValue: 'foobar', - }, - }); - }); - - it('defaultValue fails if the type is not string', () => { - expect( - PathReporter.report( - TextCustomFieldConfigurationRt.decode({ - ...defaultRequest, - required: true, - defaultValue: false, - }) - )[0] - ).toContain('Invalid value false supplied'); - }); - - it('removes foo:bar attributes from request', () => { - const query = TextCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('zod: has expected attributes with required: false', () => { + it('has expected attributes with required: false', () => { const result = TextCustomFieldConfigurationSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: has expected attributes with defaultValue', () => { + it('has expected attributes with defaultValue', () => { const result = TextCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, required: true, @@ -383,7 +239,7 @@ describe('configure', () => { }); }); - it('zod: fails if defaultValue is not string', () => { + it('fails if defaultValue is not string', () => { const result = TextCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, required: true, @@ -392,7 +248,7 @@ describe('configure', () => { expect(result.success).toBe(false); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = TextCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -402,7 +258,7 @@ describe('configure', () => { }); }); - describe('ToggleCustomFieldConfigurationRt', () => { + describe('ToggleCustomFieldConfigurationSchema', () => { const defaultRequest = { key: 'my_toggle_custom_field', label: 'Toggle Custom Field', @@ -410,60 +266,13 @@ describe('configure', () => { required: false, }; - it('has expected attributes in request with required: false', () => { - const query = ToggleCustomFieldConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('has expected attributes in request with defaultValue and required: true', () => { - const query = ToggleCustomFieldConfigurationRt.decode({ - ...defaultRequest, - required: true, - defaultValue: false, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - required: true, - defaultValue: false, - }, - }); - }); - - it('defaultValue fails if the type is not boolean', () => { - expect( - PathReporter.report( - ToggleCustomFieldConfigurationRt.decode({ - ...defaultRequest, - required: true, - defaultValue: 'foobar', - }) - )[0] - ).toContain('Invalid value "foobar" supplied'); - }); - - it('removes foo:bar attributes from request', () => { - const query = ToggleCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('zod: has expected attributes with required: false', () => { + it('has expected attributes with required: false', () => { const result = ToggleCustomFieldConfigurationSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: has expected attributes with defaultValue', () => { + it('has expected attributes with defaultValue', () => { const result = ToggleCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, required: true, @@ -473,7 +282,7 @@ describe('configure', () => { expect(result.data).toStrictEqual({ ...defaultRequest, required: true, defaultValue: false }); }); - it('zod: fails if defaultValue is not boolean', () => { + it('fails if defaultValue is not boolean', () => { const result = ToggleCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, required: true, @@ -482,7 +291,7 @@ describe('configure', () => { expect(result.success).toBe(false); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ToggleCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -492,7 +301,7 @@ describe('configure', () => { }); }); - describe('NumberCustomFieldConfigurationRt', () => { + describe('NumberCustomFieldConfigurationSchema', () => { const defaultRequest = { key: 'my_number_custom_field', label: 'Number Custom Field', @@ -500,60 +309,13 @@ describe('configure', () => { required: false, }; - it('has expected attributes in request with required: false', () => { - const query = NumberCustomFieldConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('has expected attributes in request with defaultValue and required: true', () => { - const query = NumberCustomFieldConfigurationRt.decode({ - ...defaultRequest, - required: true, - defaultValue: 0, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - required: true, - defaultValue: 0, - }, - }); - }); - - it('defaultValue fails if the type is not number', () => { - expect( - PathReporter.report( - NumberCustomFieldConfigurationRt.decode({ - ...defaultRequest, - required: true, - defaultValue: 'foobar', - }) - )[0] - ).toContain('Invalid value "foobar" supplied'); - }); - - it('removes foo:bar attributes from request', () => { - const query = NumberCustomFieldConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('zod: has expected attributes with required: false', () => { + it('has expected attributes with required: false', () => { const result = NumberCustomFieldConfigurationSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: has expected attributes with defaultValue', () => { + it('has expected attributes with defaultValue', () => { const result = NumberCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, required: true, @@ -563,7 +325,7 @@ describe('configure', () => { expect(result.data).toStrictEqual({ ...defaultRequest, required: true, defaultValue: 0 }); }); - it('zod: fails if defaultValue is not number', () => { + it('fails if defaultValue is not number', () => { const result = NumberCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, required: true, @@ -572,7 +334,7 @@ describe('configure', () => { expect(result.success).toBe(false); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = NumberCustomFieldConfigurationSchema.safeParse({ ...defaultRequest, foo: 'bar', @@ -582,94 +344,46 @@ describe('configure', () => { }); }); - describe('TemplateConfigurationRt', () => { + describe('TemplateConfigurationSchema', () => { const defaultRequest = templateWithAllCaseFields; - it('has expected attributes in request ', () => { - const query = TemplateConfigurationRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = TemplateConfigurationRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); + it('has expected attributes in request', () => { + const result = TemplateConfigurationSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from caseFields', () => { - const query = TemplateConfigurationRt.decode({ + it('strips unknown fields', () => { + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, - caseFields: { ...templateWithAllCaseFields.caseFields, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest }, - }); - }); - - it('accepts few caseFields', () => { - const query = TemplateConfigurationRt.decode(templateWithFewCaseFields); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...templateWithFewCaseFields }, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts null for caseFields', () => { - const query = TemplateConfigurationRt.decode({ + const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, caseFields: null, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, caseFields: null }, - }); - }); - - it('accepts {} for caseFields', () => { - const query = TemplateConfigurationRt.decode({ - ...defaultRequest, - caseFields: {}, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, caseFields: {} }, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = TemplateConfigurationSchema.safeParse(defaultRequest); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + expect(result.data).toStrictEqual({ ...defaultRequest, caseFields: null }); }); - it('zod: strips unknown fields', () => { - const result = TemplateConfigurationSchema.safeParse({ - ...defaultRequest, - foo: 'bar', - }); + it('accepts few caseFields', () => { + const result = TemplateConfigurationSchema.safeParse(templateWithFewCaseFields); expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); + expect(result.data).toStrictEqual(templateWithFewCaseFields); }); - it('zod: accepts null for caseFields', () => { + it('accepts {} for caseFields', () => { const result = TemplateConfigurationSchema.safeParse({ ...defaultRequest, - caseFields: null, + caseFields: {}, }); expect(result.success).toBe(true); - expect(result.data).toStrictEqual({ ...defaultRequest, caseFields: null }); + expect(result.data).toStrictEqual({ ...defaultRequest, caseFields: {} }); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.ts index b7d9a09791590..1cd77881cf91e 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/configure/v1.ts @@ -5,173 +5,147 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { CaseConnectorRt, ConnectorMappingsRt } from '../connector/v1'; -import { UserRt } from '../user/v1'; +import { z } from '@kbn/zod/v4'; +import { CaseConnectorSchema, ConnectorMappingsSchema } from '../connector/v1'; +import { UserSchema } from '../user/v1'; import { - CustomFieldTextTypeRt, - CustomFieldToggleTypeRt, - CustomFieldNumberTypeRt, + CustomFieldTextTypeSchema, + CustomFieldToggleTypeSchema, + CustomFieldNumberTypeSchema, } from '../custom_field/v1'; -import { CaseBaseOptionalFieldsRt } from '../case/v1'; -import { CaseObservableTypeRt } from '../observable/v1'; +import { CaseBaseOptionalFieldsSchema } from '../case/v1'; +import { CaseObservableTypeSchema } from '../observable/v1'; -export const ClosureTypeRt = rt.union([ - rt.literal('close-by-user'), - rt.literal('close-by-pushing'), +export const ClosureTypeSchema = z.union([ + z.literal('close-by-user'), + z.literal('close-by-pushing'), ]); -export const CustomFieldConfigurationWithoutTypeRt = rt.strict({ +export const CustomFieldConfigurationWithoutTypeSchema = z.object({ /** * key of custom field */ - key: rt.string, + key: z.string(), /** * label of custom field */ - label: rt.string, + label: z.string(), /** * custom field options - required */ - required: rt.boolean, + required: z.boolean(), }); -export const TextCustomFieldConfigurationRt = rt.intersection([ - rt.strict({ type: CustomFieldTextTypeRt }), - CustomFieldConfigurationWithoutTypeRt, - rt.exact( - rt.partial({ - defaultValue: rt.union([rt.string, rt.null]), - }) - ), -]); +export const TextCustomFieldConfigurationSchema = CustomFieldConfigurationWithoutTypeSchema.extend({ + type: CustomFieldTextTypeSchema, + defaultValue: z.string().nullable().optional(), +}); -export const ToggleCustomFieldConfigurationRt = rt.intersection([ - rt.strict({ type: CustomFieldToggleTypeRt }), - CustomFieldConfigurationWithoutTypeRt, - rt.exact( - rt.partial({ - defaultValue: rt.union([rt.boolean, rt.null]), - }) - ), +export const ToggleCustomFieldConfigurationSchema = + CustomFieldConfigurationWithoutTypeSchema.extend({ + type: CustomFieldToggleTypeSchema, + defaultValue: z.boolean().nullable().optional(), + }); + +export const NumberCustomFieldConfigurationSchema = + CustomFieldConfigurationWithoutTypeSchema.extend({ + type: CustomFieldNumberTypeSchema, + defaultValue: z.number().nullable().optional(), + }); + +export const CustomFieldConfigurationSchema = z.union([ + TextCustomFieldConfigurationSchema, + ToggleCustomFieldConfigurationSchema, + NumberCustomFieldConfigurationSchema, ]); -export const NumberCustomFieldConfigurationRt = rt.intersection([ - rt.strict({ type: CustomFieldNumberTypeRt }), - CustomFieldConfigurationWithoutTypeRt, - rt.exact( - rt.partial({ - defaultValue: rt.union([rt.number, rt.null]), - }) - ), -]); +export const CustomFieldsConfigurationSchema = z.array(CustomFieldConfigurationSchema); -export const CustomFieldConfigurationRt = rt.union([ - TextCustomFieldConfigurationRt, - ToggleCustomFieldConfigurationRt, - NumberCustomFieldConfigurationRt, -]); +export const ObservableTypesConfigurationSchema = z.array(CaseObservableTypeSchema); -export const CustomFieldsConfigurationRt = rt.array(CustomFieldConfigurationRt); - -export const ObservableTypesConfigurationRt = rt.array(CaseObservableTypeRt); - -export const TemplateConfigurationRt = rt.intersection([ - rt.strict({ - /** - * key of template - */ - key: rt.string, - /** - * name of template - */ - name: rt.string, - /** - * case fields of template - */ - caseFields: rt.union([rt.null, CaseBaseOptionalFieldsRt]), - }), - rt.exact( - rt.partial({ - /** - * description of template - */ - description: rt.string, - /** - * tags of template - */ - tags: rt.array(rt.string), - }) - ), -]); +export const TemplateConfigurationSchema = z.object({ + /** + * key of template + */ + key: z.string(), + /** + * name of template + */ + name: z.string(), + /** + * case fields of template + */ + caseFields: CaseBaseOptionalFieldsSchema.nullable(), + /** + * description of template + */ + description: z.string().optional(), + /** + * tags of template + */ + tags: z.array(z.string()).optional(), +}); -export const TemplatesConfigurationRt = rt.array(TemplateConfigurationRt); +export const TemplatesConfigurationSchema = z.array(TemplateConfigurationSchema); -export const ConfigurationBasicWithoutOwnerRt = rt.strict({ +export const ConfigurationBasicWithoutOwnerSchema = z.object({ /** * The external connector */ - connector: CaseConnectorRt, + connector: CaseConnectorSchema, /** * Whether to close the case after it has been synced with the external system */ - closure_type: ClosureTypeRt, + closure_type: ClosureTypeSchema, /** * The custom fields configured for the case */ - customFields: CustomFieldsConfigurationRt, + customFields: CustomFieldsConfigurationSchema, /** * Templates configured for the case */ - templates: TemplatesConfigurationRt, + templates: TemplatesConfigurationSchema, /** * Observable types configured for the case */ - observableTypes: ObservableTypesConfigurationRt, + observableTypes: ObservableTypesConfigurationSchema, }); -export const CasesConfigureBasicRt = rt.intersection([ - ConfigurationBasicWithoutOwnerRt, - rt.strict({ - /** - * The plugin owner that manages this configuration - */ - owner: rt.string, - }), -]); +export const CasesConfigureBasicSchema = ConfigurationBasicWithoutOwnerSchema.extend({ + /** + * The plugin owner that manages this configuration + */ + owner: z.string(), +}); -export const ConfigurationActivityFieldsRt = rt.strict({ - created_at: rt.string, - created_by: UserRt, - updated_at: rt.union([rt.string, rt.null]), - updated_by: rt.union([UserRt, rt.null]), +export const ConfigurationActivityFieldsSchema = z.object({ + created_at: z.string(), + created_by: UserSchema, + updated_at: z.string().nullable(), + updated_by: UserSchema.nullable(), }); -export const ConfigurationAttributesRt = rt.intersection([ - CasesConfigureBasicRt, - ConfigurationActivityFieldsRt, -]); +export const ConfigurationAttributesSchema = CasesConfigureBasicSchema.merge( + ConfigurationActivityFieldsSchema +); -export const ConfigurationRt = rt.intersection([ - ConfigurationAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - error: rt.union([rt.string, rt.null]), - owner: rt.string, - mappings: ConnectorMappingsRt, - }), -]); +export const ConfigurationSchema = ConfigurationAttributesSchema.extend({ + id: z.string(), + version: z.string(), + error: z.string().nullable(), + owner: z.string(), + mappings: ConnectorMappingsSchema, +}); -export const ConfigurationsRt = rt.array(ConfigurationRt); - -export type CustomFieldsConfiguration = rt.TypeOf; -export type CustomFieldConfiguration = rt.TypeOf; -export type TemplatesConfiguration = rt.TypeOf; -export type TemplateConfiguration = rt.TypeOf; -export type ClosureType = rt.TypeOf; -export type ConfigurationAttributes = rt.TypeOf; -export type Configuration = rt.TypeOf; -export type Configurations = rt.TypeOf; -export type ObservableTypesConfiguration = rt.TypeOf; -export type ObservableTypeConfiguration = rt.TypeOf; +export const ConfigurationsSchema = z.array(ConfigurationSchema); + +export type CustomFieldsConfiguration = z.infer; +export type CustomFieldConfiguration = z.infer; +export type TemplatesConfiguration = z.infer; +export type TemplateConfiguration = z.infer; +export type ClosureType = z.infer; +export type ConfigurationAttributes = z.infer; +export type Configuration = z.infer; +export type Configurations = z.infer; +export type ObservableTypesConfiguration = z.infer; +export type ObservableTypeConfiguration = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.test.ts index 74761a84d7265..8df7d939ff15a 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.test.ts @@ -6,23 +6,16 @@ */ import { + ConnectorTypes, ConnectorTypeFieldsSchema, CaseUserActionConnectorSchema, CaseConnectorSchema, ConnectorMappingsSchema, ConnectorMappingsAttributesSchema, -} from '../../domain_zod/connector/v1'; -import { - ConnectorTypeFieldsRt, - CaseUserActionConnectorRt, - CaseConnectorRt, - ConnectorTypes, - ConnectorMappingsAttributesRt, - ConnectorMappingsRt, } from './v1'; describe('Connector', () => { - describe('ConnectorTypeFieldsRt', () => { + describe('ConnectorTypeFieldsSchema', () => { const defaultRequest = { type: ConnectorTypes.jira, fields: { @@ -33,51 +26,18 @@ describe('Connector', () => { }; it('has expected attributes in request', () => { - const query = ConnectorTypeFieldsRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ConnectorTypeFieldsRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from fields', () => { - const query = ConnectorTypeFieldsRt.decode({ - ...defaultRequest, - fields: { ...defaultRequest.fields, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ConnectorTypeFieldsSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ConnectorTypeFieldsSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields from fields', () => { + it('strips unknown fields from fields', () => { const result = ConnectorTypeFieldsSchema.safeParse({ ...defaultRequest, fields: { ...defaultRequest.fields, foo: 'bar' }, @@ -87,7 +47,7 @@ describe('Connector', () => { }); }); - describe('CaseUserActionConnectorRt', () => { + describe('CaseUserActionConnectorSchema', () => { const defaultRequest = { type: ConnectorTypes.jira, name: 'jira connector', @@ -99,52 +59,28 @@ describe('Connector', () => { }; it('has expected attributes in request', () => { - const query = CaseUserActionConnectorRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CaseUserActionConnectorSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CaseUserActionConnectorRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CaseUserActionConnectorSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from fields', () => { - const query = CaseUserActionConnectorRt.decode({ + it('strips unknown fields from fields', () => { + const result = CaseUserActionConnectorSchema.safeParse({ ...defaultRequest, fields: { ...defaultRequest.fields, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = CaseUserActionConnectorSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = CaseUserActionConnectorSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CaseConnectorRt', () => { + describe('CaseConnectorSchema', () => { const defaultRequest = { type: ConnectorTypes.jira, name: 'jira connector', @@ -157,46 +93,22 @@ describe('Connector', () => { }; it('has expected attributes in request', () => { - const query = CaseConnectorRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CaseConnectorSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CaseConnectorRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CaseConnectorSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from fields', () => { - const query = CaseConnectorRt.decode({ + it('strips unknown fields from fields', () => { + const result = CaseConnectorSchema.safeParse({ ...defaultRequest, fields: { ...defaultRequest.fields, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { - const result = CaseConnectorSchema.safeParse(defaultRequest); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(defaultRequest); - }); - - it('zod: strips unknown fields', () => { - const result = CaseConnectorSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); @@ -232,40 +144,14 @@ describe('Connector', () => { owner: 'cases', }; - describe('ConnectorMappingsRt', () => { + describe('ConnectorMappingsSchema', () => { it('has expected attributes in request', () => { - const query = ConnectorMappingsRt.decode(mappings); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: mappings, - }); - }); - - it('removes foo:bar attributes from mappings', () => { - const query = ConnectorMappingsRt.decode([ - { ...mappings[0] }, - { - action_type: 'append', - source: 'description', - target: 'not_mapped', - foo: 'bar', - }, - ]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: mappings, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ConnectorMappingsSchema.safeParse(mappings); expect(result.success).toBe(true); expect(result.data).toStrictEqual(mappings); }); - it('zod: strips unknown fields from mappings', () => { + it('strips unknown fields from mappings', () => { const result = ConnectorMappingsSchema.safeParse([ { ...mappings[0] }, { action_type: 'append', source: 'description', target: 'not_mapped', foo: 'bar' }, @@ -275,50 +161,20 @@ describe('Connector', () => { }); }); - describe('ConnectorMappingsAttributesRt', () => { + describe('ConnectorMappingsAttributesSchema', () => { it('has expected attributes in request', () => { - const query = ConnectorMappingsAttributesRt.decode(attributes); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: attributes, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ConnectorMappingsAttributesRt.decode({ ...attributes, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: attributes, - }); - }); - - it('removes foo:bar attributes from mappings', () => { - const query = ConnectorMappingsAttributesRt.decode({ - ...attributes, - mappings: [{ ...attributes.mappings[0], foo: 'bar' }], - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...attributes, mappings: [{ ...attributes.mappings[0] }] }, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ConnectorMappingsAttributesSchema.safeParse(attributes); expect(result.success).toBe(true); expect(result.data).toStrictEqual(attributes); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ConnectorMappingsAttributesSchema.safeParse({ ...attributes, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(attributes); }); - it('zod: strips unknown fields from mappings', () => { + it('strips unknown fields from mappings', () => { const result = ConnectorMappingsAttributesSchema.safeParse({ ...attributes, mappings: [{ ...attributes.mappings[0], foo: 'bar' }], diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.ts index b9eb892e65fe7..a4d5789281b62 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/connector/v1.ts @@ -5,14 +5,10 @@ * 2.0. */ -import * as rt from 'io-ts'; - +import { z } from '@kbn/zod/v4'; import type { ActionType as ConnectorActionType } from '@kbn/actions-plugin/common'; import type { ActionResult } from '@kbn/actions-plugin/server/types'; -export type ActionConnector = ActionResult; -export type ActionTypeConnector = ConnectorActionType; - export enum ConnectorTypes { casesWebhook = '.cases-webhook', jira = '.jira', @@ -24,238 +20,207 @@ export enum ConnectorTypes { theHive = '.thehive', } -const ConnectorCasesWebhookTypeFieldsRt = rt.strict({ - type: rt.literal(ConnectorTypes.casesWebhook), - fields: rt.null, +export enum SwimlaneConnectorType { + All = 'all', + Alerts = 'alerts', + Cases = 'cases', +} + +export type ActionConnector = ActionResult; +export type ActionTypeConnector = ConnectorActionType; + +const ConnectorCasesWebhookTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.casesWebhook), + fields: z.null(), }); /** * Jira */ +export const JiraFieldsSchema = z.object({ + issueType: z.string().nullable(), + priority: z.string().nullable(), + parent: z.string().nullable(), + otherFields: z.string().nullable().optional(), +}); -export const JiraFieldsRt = rt.intersection([ - rt.strict({ - issueType: rt.union([rt.string, rt.null]), - priority: rt.union([rt.string, rt.null]), - parent: rt.union([rt.string, rt.null]), - }), - rt.exact( - rt.partial({ - otherFields: rt.union([rt.string, rt.null]), - }) - ), -]); - -export type JiraFieldsType = rt.TypeOf; - -const ConnectorJiraTypeFieldsRt = rt.strict({ - type: rt.literal(ConnectorTypes.jira), - fields: rt.union([JiraFieldsRt, rt.null]), +const ConnectorJiraTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.jira), + fields: JiraFieldsSchema.nullable(), }); /** * Resilient */ +export const ResilientFieldsSchema = z.object({ + incidentTypes: z.array(z.string()).nullable(), + severityCode: z.string().nullable(), + additionalFields: z.string().nullable().optional(), +}); -export const ResilientFieldsRt = rt.intersection([ - rt.strict({ - incidentTypes: rt.union([rt.array(rt.string), rt.null]), - severityCode: rt.union([rt.string, rt.null]), - }), - rt.exact( - rt.partial({ - additionalFields: rt.union([rt.string, rt.null]), - }) - ), -]); - -export type ResilientFieldsType = rt.TypeOf; - -const ConnectorResilientTypeFieldsRt = rt.intersection([ - rt.strict({ - type: rt.literal(ConnectorTypes.resilient), - fields: rt.union([ResilientFieldsRt, rt.null]), - }), - rt.exact( - rt.partial({ - additionalFields: rt.union([rt.string, rt.null]), - }) - ), -]); +const ConnectorResilientTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.resilient), + fields: ResilientFieldsSchema.nullable(), +}); /** - * ServiceNow + * ServiceNow ITSM */ - -export const ServiceNowITSMFieldsRt = rt.intersection([ - rt.strict({ - impact: rt.union([rt.string, rt.null]), - severity: rt.union([rt.string, rt.null]), - urgency: rt.union([rt.string, rt.null]), - category: rt.union([rt.string, rt.null]), - subcategory: rt.union([rt.string, rt.null]), - }), - rt.exact( - rt.partial({ - additionalFields: rt.union([rt.string, rt.null]), - }) - ), -]); - -export type ServiceNowITSMFieldsType = rt.TypeOf; - -const ConnectorServiceNowITSMTypeFieldsRt = rt.strict({ - type: rt.literal(ConnectorTypes.serviceNowITSM), - fields: rt.union([ServiceNowITSMFieldsRt, rt.null]), +export const ServiceNowITSMFieldsSchema = z.object({ + impact: z.string().nullable(), + severity: z.string().nullable(), + urgency: z.string().nullable(), + category: z.string().nullable(), + subcategory: z.string().nullable(), + additionalFields: z.string().nullable().optional(), }); -export const ServiceNowSIRFieldsRt = rt.intersection([ - rt.strict({ - category: rt.union([rt.string, rt.null]), - destIp: rt.union([rt.boolean, rt.null]), - malwareHash: rt.union([rt.boolean, rt.null]), - malwareUrl: rt.union([rt.boolean, rt.null]), - priority: rt.union([rt.string, rt.null]), - sourceIp: rt.union([rt.boolean, rt.null]), - subcategory: rt.union([rt.string, rt.null]), - }), - rt.exact( - rt.partial({ - additionalFields: rt.union([rt.string, rt.null]), - }) - ), -]); +const ConnectorServiceNowITSMTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.serviceNowITSM), + fields: ServiceNowITSMFieldsSchema.nullable(), +}); -export type ServiceNowSIRFieldsType = rt.TypeOf; +/** + * ServiceNow SIR + */ +export const ServiceNowSIRFieldsSchema = z.object({ + category: z.string().nullable(), + destIp: z.boolean().nullable(), + malwareHash: z.boolean().nullable(), + malwareUrl: z.boolean().nullable(), + priority: z.string().nullable(), + sourceIp: z.boolean().nullable(), + subcategory: z.string().nullable(), + additionalFields: z.string().nullable().optional(), +}); -const ConnectorServiceNowSIRTypeFieldsRt = rt.strict({ - type: rt.literal(ConnectorTypes.serviceNowSIR), - fields: rt.union([ServiceNowSIRFieldsRt, rt.null]), +const ConnectorServiceNowSIRTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.serviceNowSIR), + fields: ServiceNowSIRFieldsSchema.nullable(), }); /** * Swimlane */ - -export const SwimlaneFieldsRt = rt.strict({ - caseId: rt.union([rt.string, rt.null]), +export const SwimlaneFieldsSchema = z.object({ + caseId: z.string().nullable(), }); -export enum SwimlaneConnectorType { - All = 'all', - Alerts = 'alerts', - Cases = 'cases', -} - -export type SwimlaneFieldsType = rt.TypeOf; - -const ConnectorSwimlaneTypeFieldsRt = rt.strict({ - type: rt.literal(ConnectorTypes.swimlane), - fields: rt.union([SwimlaneFieldsRt, rt.null]), +const ConnectorSwimlaneTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.swimlane), + fields: SwimlaneFieldsSchema.nullable(), }); /** - * Thehive + * TheHive */ - -export const TheHiveFieldsRt = rt.strict({ - tlp: rt.union([rt.number, rt.null]), +export const TheHiveFieldsSchema = z.object({ + tlp: z.number().nullable(), }); -export type TheHiveFieldsType = rt.TypeOf; - -const ConnectorTheHiveTypeFieldsRt = rt.strict({ - type: rt.literal(ConnectorTypes.theHive), - fields: rt.union([TheHiveFieldsRt, rt.null]), +const ConnectorTheHiveTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.theHive), + fields: TheHiveFieldsSchema.nullable(), }); /** * None connector */ - -const ConnectorNoneTypeFieldsRt = rt.strict({ - type: rt.literal(ConnectorTypes.none), - fields: rt.null, +const ConnectorNoneTypeFieldsSchema = z.object({ + type: z.literal(ConnectorTypes.none), + fields: z.null(), }); -export const ConnectorTypeFieldsRt = rt.union([ - ConnectorCasesWebhookTypeFieldsRt, - ConnectorJiraTypeFieldsRt, - ConnectorNoneTypeFieldsRt, - ConnectorResilientTypeFieldsRt, - ConnectorServiceNowITSMTypeFieldsRt, - ConnectorServiceNowSIRTypeFieldsRt, - ConnectorSwimlaneTypeFieldsRt, - ConnectorTheHiveTypeFieldsRt, +export const ConnectorTypeFieldsSchema = z.discriminatedUnion('type', [ + ConnectorCasesWebhookTypeFieldsSchema, + ConnectorJiraTypeFieldsSchema, + ConnectorNoneTypeFieldsSchema, + ConnectorResilientTypeFieldsSchema, + ConnectorServiceNowITSMTypeFieldsSchema, + ConnectorServiceNowSIRTypeFieldsSchema, + ConnectorSwimlaneTypeFieldsSchema, + ConnectorTheHiveTypeFieldsSchema, ]); -/** - * This type represents the connector's format when it is encoded within a user action. - */ -export const CaseUserActionConnectorRt = rt.union([ - rt.intersection([ConnectorCasesWebhookTypeFieldsRt, rt.strict({ name: rt.string })]), - rt.intersection([ConnectorJiraTypeFieldsRt, rt.strict({ name: rt.string })]), - rt.intersection([ConnectorNoneTypeFieldsRt, rt.strict({ name: rt.string })]), - rt.intersection([ConnectorResilientTypeFieldsRt, rt.strict({ name: rt.string })]), - rt.intersection([ConnectorServiceNowITSMTypeFieldsRt, rt.strict({ name: rt.string })]), - rt.intersection([ConnectorServiceNowSIRTypeFieldsRt, rt.strict({ name: rt.string })]), - rt.intersection([ConnectorSwimlaneTypeFieldsRt, rt.strict({ name: rt.string })]), - rt.intersection([ConnectorTheHiveTypeFieldsRt, rt.strict({ name: rt.string })]), +const NameSchema = z.object({ name: z.string() }); + +export const CaseUserActionConnectorSchema = z.discriminatedUnion('type', [ + ConnectorCasesWebhookTypeFieldsSchema.merge(NameSchema), + ConnectorJiraTypeFieldsSchema.merge(NameSchema), + ConnectorNoneTypeFieldsSchema.merge(NameSchema), + ConnectorResilientTypeFieldsSchema.merge(NameSchema), + ConnectorServiceNowITSMTypeFieldsSchema.merge(NameSchema), + ConnectorServiceNowSIRTypeFieldsSchema.merge(NameSchema), + ConnectorSwimlaneTypeFieldsSchema.merge(NameSchema), + ConnectorTheHiveTypeFieldsSchema.merge(NameSchema), ]); -export const CaseConnectorRt = rt.intersection([ - rt.strict({ - id: rt.string, - }), - CaseUserActionConnectorRt, +const IdSchema = z.object({ id: z.string() }); + +export const CaseConnectorSchema = z.discriminatedUnion('type', [ + ConnectorCasesWebhookTypeFieldsSchema.merge(NameSchema).merge(IdSchema), + ConnectorJiraTypeFieldsSchema.merge(NameSchema).merge(IdSchema), + ConnectorNoneTypeFieldsSchema.merge(NameSchema).merge(IdSchema), + ConnectorResilientTypeFieldsSchema.merge(NameSchema).merge(IdSchema), + ConnectorServiceNowITSMTypeFieldsSchema.merge(NameSchema).merge(IdSchema), + ConnectorServiceNowSIRTypeFieldsSchema.merge(NameSchema).merge(IdSchema), + ConnectorSwimlaneTypeFieldsSchema.merge(NameSchema).merge(IdSchema), + ConnectorTheHiveTypeFieldsSchema.merge(NameSchema).merge(IdSchema), ]); /** * Mappings */ - -const ConnectorMappingActionTypeRt = rt.union([ - rt.literal('append'), - rt.literal('nothing'), - rt.literal('overwrite'), +const ConnectorMappingActionTypeSchema = z.union([ + z.literal('append'), + z.literal('nothing'), + z.literal('overwrite'), ]); -const ConnectorMappingSourceRt = rt.union([ - rt.literal('title'), - rt.literal('description'), - rt.literal('comments'), - rt.literal('tags'), +const ConnectorMappingSourceSchema = z.union([ + z.literal('title'), + z.literal('description'), + z.literal('comments'), + z.literal('tags'), ]); -const ConnectorMappingTargetRt = rt.union([rt.string, rt.literal('not_mapped')]); +const ConnectorMappingTargetSchema = z.union([z.string(), z.literal('not_mapped')]); -const ConnectorMappingRt = rt.strict({ - action_type: ConnectorMappingActionTypeRt, - source: ConnectorMappingSourceRt, - target: ConnectorMappingTargetRt, +const ConnectorMappingSchema = z.object({ + action_type: ConnectorMappingActionTypeSchema, + source: ConnectorMappingSourceSchema, + target: ConnectorMappingTargetSchema, }); -export const ConnectorMappingsRt = rt.array(ConnectorMappingRt); +export const ConnectorMappingsSchema = z.array(ConnectorMappingSchema); -export const ConnectorMappingsAttributesRt = rt.strict({ - mappings: ConnectorMappingsRt, - owner: rt.string, +export const ConnectorMappingsAttributesSchema = z.object({ + mappings: ConnectorMappingsSchema, + owner: z.string(), }); -export type ConnectorMappingsAttributes = rt.TypeOf; -export type ConnectorMappings = rt.TypeOf; -export type ConnectorMappingActionType = rt.TypeOf; -export type ConnectorMappingSource = rt.TypeOf; -export type ConnectorMappingTarget = rt.TypeOf; -export type CaseUserActionConnector = rt.TypeOf; -export type CaseConnector = rt.TypeOf; -export type ConnectorTypeFields = rt.TypeOf; -export type ConnectorCasesWebhookTypeFields = rt.TypeOf; -export type ConnectorJiraTypeFields = rt.TypeOf; -export type ConnectorResilientTypeFields = rt.TypeOf; -export type ConnectorSwimlaneTypeFields = rt.TypeOf; -export type ConnectorServiceNowITSMTypeFields = rt.TypeOf< - typeof ConnectorServiceNowITSMTypeFieldsRt +export type ConnectorMappingsAttributes = z.infer; +export type ConnectorMappings = z.infer; +export type ConnectorMappingActionType = z.infer; +export type ConnectorMappingSource = z.infer; +export type ConnectorMappingTarget = z.infer; +export type CaseUserActionConnector = z.infer; +export type CaseConnector = z.infer; +export type ConnectorTypeFields = z.infer; +export type JiraFieldsType = z.infer; +export type ResilientFieldsType = z.infer; +export type SwimlaneFieldsType = z.infer; +export type ServiceNowITSMFieldsType = z.infer; +export type ServiceNowSIRFieldsType = z.infer; +export type TheHiveFieldsType = z.infer; +export type ConnectorCasesWebhookTypeFields = z.infer; +export type ConnectorJiraTypeFields = z.infer; +export type ConnectorResilientTypeFields = z.infer; +export type ConnectorServiceNowITSMTypeFields = z.infer< + typeof ConnectorServiceNowITSMTypeFieldsSchema +>; +export type ConnectorServiceNowSIRTypeFields = z.infer< + typeof ConnectorServiceNowSIRTypeFieldsSchema >; -export type ConnectorServiceNowSIRTypeFields = rt.TypeOf; -export type ConnectorTheHiveTypeFields = rt.TypeOf; +export type ConnectorSwimlaneTypeFields = z.infer; +export type ConnectorTheHiveTypeFields = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.test.ts index 37981203a4e7e..159d35dd67b71 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.test.ts @@ -5,11 +5,9 @@ * 2.0. */ -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { CaseCustomFieldRt } from './v1'; -import { CaseCustomFieldSchema } from '../../domain_zod/custom_field/v1'; +import { CaseCustomFieldSchema } from './v1'; -describe('CaseCustomFieldRt', () => { +describe('CaseCustomFieldSchema', () => { it.each([ [ 'type text value text', @@ -59,62 +57,13 @@ describe('CaseCustomFieldRt', () => { value: null, }, ], - ])(`has expected attributes for customField with %s`, (_, customField) => { - const query = CaseCustomFieldRt.decode(customField); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: customField, - }); - }); - - it('fails if text type and value do not match expected attributes in request', () => { - const query = CaseCustomFieldRt.decode({ - key: 'text_custom_field_1', - type: 'text', - value: 1, - }); - - expect(PathReporter.report(query)[0]).toContain('Invalid value 1 supplied'); - }); - - it('fails if toggle type and value do not match expected attributes in request', () => { - const query = CaseCustomFieldRt.decode({ - key: 'list_custom_field_1', - type: 'toggle', - value: 'hello', - }); - - expect(PathReporter.report(query)[0]).toContain('Invalid value "hello" supplied'); - }); - - it('fails if number type but value is a string', () => { - const query = CaseCustomFieldRt.decode({ - key: 'list_custom_field_1', - type: 'number', - value: 'hi', - }); - - expect(PathReporter.report(query)[0]).toContain('Invalid value "hi" supplied'); - }); - - it.each([ - [ - 'type text value text', - { key: 'string_custom_field_1', type: 'text', value: 'this is a text field value' }, - ], - ['type text value null', { key: 'string_custom_field_2', type: 'text', value: null }], - ['type toggle value boolean', { key: 'toggle_custom_field_1', type: 'toggle', value: true }], - ['type toggle value null', { key: 'toggle_custom_field_2', type: 'toggle', value: null }], - ['type number value number', { key: 'number_custom_field_1', type: 'number', value: 1 }], - ['type number value null', { key: 'number_custom_field_2', type: 'number', value: null }], - ])('zod: has expected attributes for customField with %s', (_, customField) => { + ])('has expected attributes for customField with %s', (_, customField) => { const result = CaseCustomFieldSchema.safeParse(customField); expect(result.success).toBe(true); expect(result.data).toStrictEqual(customField); }); - it('zod: fails if text type and value do not match', () => { + it('fails if text type and value do not match', () => { const result = CaseCustomFieldSchema.safeParse({ key: 'text_custom_field_1', type: 'text', @@ -123,7 +72,7 @@ describe('CaseCustomFieldRt', () => { expect(result.success).toBe(false); }); - it('zod: fails if toggle type and value do not match', () => { + it('fails if toggle type and value do not match', () => { const result = CaseCustomFieldSchema.safeParse({ key: 'list_custom_field_1', type: 'toggle', @@ -132,7 +81,7 @@ describe('CaseCustomFieldRt', () => { expect(result.success).toBe(false); }); - it('zod: fails if number type but value is a string', () => { + it('fails if number type but value is a string', () => { const result = CaseCustomFieldSchema.safeParse({ key: 'list_custom_field_1', type: 'number', diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.ts index d0f9404f8f113..f35dc64e2554f 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/custom_field/v1.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as rt from 'io-ts'; + +import { z } from '@kbn/zod/v4'; export enum CustomFieldTypes { TEXT = 'text', @@ -12,37 +13,38 @@ export enum CustomFieldTypes { NUMBER = 'number', } -export const CustomFieldTextTypeRt = rt.literal(CustomFieldTypes.TEXT); -export const CustomFieldToggleTypeRt = rt.literal(CustomFieldTypes.TOGGLE); -export const CustomFieldNumberTypeRt = rt.literal(CustomFieldTypes.NUMBER); +export const CustomFieldTextTypeSchema = z.literal(CustomFieldTypes.TEXT); +export const CustomFieldToggleTypeSchema = z.literal(CustomFieldTypes.TOGGLE); +export const CustomFieldNumberTypeSchema = z.literal(CustomFieldTypes.NUMBER); -const CaseCustomFieldTextRt = rt.strict({ - key: rt.string, - type: CustomFieldTextTypeRt, - value: rt.union([rt.string, rt.null]), +const CaseCustomFieldTextSchema = z.object({ + key: z.string(), + type: CustomFieldTextTypeSchema, + value: z.string().nullable(), }); -export const CaseCustomFieldToggleRt = rt.strict({ - key: rt.string, - type: CustomFieldToggleTypeRt, - value: rt.union([rt.boolean, rt.null]), +export const CaseCustomFieldToggleSchema = z.object({ + key: z.string(), + type: CustomFieldToggleTypeSchema, + value: z.boolean().nullable(), }); -export const CaseCustomFieldNumberRt = rt.strict({ - key: rt.string, - type: CustomFieldNumberTypeRt, - value: rt.union([rt.number, rt.null]), +export const CaseCustomFieldNumberSchema = z.object({ + key: z.string(), + type: CustomFieldNumberTypeSchema, + value: z.number().nullable(), }); -export const CaseCustomFieldRt = rt.union([ - CaseCustomFieldTextRt, - CaseCustomFieldToggleRt, - CaseCustomFieldNumberRt, +export const CaseCustomFieldSchema = z.union([ + CaseCustomFieldTextSchema, + CaseCustomFieldToggleSchema, + CaseCustomFieldNumberSchema, ]); -export const CaseCustomFieldsRt = rt.array(CaseCustomFieldRt); -export type CaseCustomFields = rt.TypeOf; -export type CaseCustomField = rt.TypeOf; -export type CaseCustomFieldToggle = rt.TypeOf; -export type CaseCustomFieldText = rt.TypeOf; -export type CaseCustomFieldNumber = rt.TypeOf; +export const CaseCustomFieldsSchema = z.array(CaseCustomFieldSchema); + +export type CaseCustomFields = z.infer; +export type CaseCustomField = z.infer; +export type CaseCustomFieldToggle = z.infer; +export type CaseCustomFieldText = z.infer; +export type CaseCustomFieldNumber = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.test.ts index 99d1eb5c184a0..970f49ca3d73f 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.test.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { ExternalServiceRt } from './v1'; -import { ExternalServiceSchema } from '../../domain_zod/external_service/v1'; +import { ExternalServiceSchema } from './v1'; -describe('ExternalServiceRt', () => { +describe('ExternalServiceSchema', () => { const defaultRequest = { connector_id: 'servicenow-1', connector_name: 'My SN connector', @@ -24,48 +23,18 @@ describe('ExternalServiceRt', () => { }; it('has expected attributes in request', () => { - const query = ExternalServiceRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = ExternalServiceRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from pushed_by', () => { - const query = ExternalServiceRt.decode({ - ...defaultRequest, - pushed_by: { ...defaultRequest.pushed_by, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = ExternalServiceSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = ExternalServiceSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields from pushed_by', () => { + it('strips unknown fields from pushed_by', () => { const result = ExternalServiceSchema.safeParse({ ...defaultRequest, pushed_by: { ...defaultRequest.pushed_by, foo: 'bar' }, diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.ts index 1d76ab7e7bb91..7d46489987c97 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/external_service/v1.ts @@ -5,27 +5,20 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { UserRt } from '../user/v1'; +import { z } from '@kbn/zod/v4'; +import { UserSchema } from '../user/v1'; -/** - * This represents the push to service UserAction. It lacks the connector_id because that is stored in a different field - * within the user action object in the API response. - */ -export const ExternalServiceBasicRt = rt.strict({ - connector_name: rt.string, - external_id: rt.string, - external_title: rt.string, - external_url: rt.string, - pushed_at: rt.string, - pushed_by: UserRt, +export const ExternalServiceBasicSchema = z.object({ + connector_name: z.string(), + external_id: z.string(), + external_title: z.string(), + external_url: z.string(), + pushed_at: z.string(), + pushed_by: UserSchema, }); -export const ExternalServiceRt = rt.intersection([ - rt.strict({ - connector_id: rt.string, - }), - ExternalServiceBasicRt, -]); +export const ExternalServiceSchema = ExternalServiceBasicSchema.extend({ + connector_id: z.string(), +}); -export type ExternalService = rt.TypeOf; +export type ExternalService = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/incremental_id/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/incremental_id/v1.ts index 7a7b3e7327e3c..83bb860f79b42 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/incremental_id/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/incremental_id/v1.ts @@ -4,10 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import * as rt from 'io-ts'; -export const CaseIdIncrementerAttributesRt = rt.strict({ - '@timestamp': rt.number, - updated_at: rt.number, - last_id: rt.number, +import { z } from '@kbn/zod/v4'; + +export const CaseIdIncrementerAttributesSchema = z.object({ + '@timestamp': z.number(), + updated_at: z.number(), + last_id: z.number(), }); + +export type CaseIdIncrementerAttributes = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.test.ts index 8a265092543b5..62dda3648d38c 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.test.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { CaseObservableRt } from './v1'; -import { CaseObservableSchema } from '../../domain_zod/observable/v1'; +import { CaseObservableSchema } from './v1'; -describe('CaseObservableRt', () => { +describe('CaseObservableSchema', () => { const observable = { description: null, id: '274fcbfc-87b8-47d0-9f17-bfe98e5453e9', @@ -19,21 +18,12 @@ describe('CaseObservableRt', () => { }; it('has expected attributes in request', () => { - const query = CaseObservableRt.decode(observable); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: observable, - }); - }); - - it('zod: has expected attributes in request', () => { const result = CaseObservableSchema.safeParse(observable); expect(result.success).toBe(true); expect(result.data).toStrictEqual(observable); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CaseObservableSchema.safeParse({ ...observable, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(observable); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.ts index 7fff862acac68..2d81dc970ab1e 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/observable/v1.ts @@ -5,27 +5,24 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; -export const CaseObservableBaseRt = rt.strict({ - typeKey: rt.string, - value: rt.string, - description: rt.union([rt.string, rt.null]), +export const CaseObservableBaseSchema = z.object({ + typeKey: z.string(), + value: z.string(), + description: z.string().nullable(), }); -export const CaseObservableRt = rt.intersection([ - rt.strict({ - id: rt.string, - createdAt: rt.string, - updatedAt: rt.union([rt.string, rt.null]), - }), - CaseObservableBaseRt, -]); +export const CaseObservableSchema = CaseObservableBaseSchema.extend({ + id: z.string(), + createdAt: z.string(), + updatedAt: z.string().nullable(), +}); -export const CaseObservableTypeRt = rt.strict({ - key: rt.string, - label: rt.string, +export const CaseObservableTypeSchema = z.object({ + key: z.string(), + label: z.string(), }); -export type Observable = rt.TypeOf; -export type ObservableType = rt.TypeOf; +export type Observable = z.infer; +export type ObservableType = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.test.ts index 2a0da46d151dd..378cdfb71080a 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.test.ts @@ -6,58 +6,37 @@ */ import { set } from '@kbn/safer-lodash-set'; -import { UserRt, UserWithProfileInfoRt, UsersRt, CaseUserProfileRt, CaseAssigneesRt } from './v1'; import { UserSchema, UserWithProfileInfoSchema, UsersSchema, CaseUserProfileSchema, CaseAssigneesSchema, -} from '../../domain_zod/user/v1'; +} from './v1'; describe('User', () => { - describe('UserRt', () => { + describe('UserSchema', () => { const defaultRequest = { full_name: 'elastic', email: 'testemail@elastic.co', username: 'elastic', profile_uid: 'profile-uid-1', }; - it('has expected attributes in request', () => { - const query = UserRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = UserRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - it('zod: has expected attributes in request', () => { + it('has expected attributes in request', () => { const result = UserSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = UserSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('UserWithProfileInfoRt', () => { + describe('UserWithProfileInfoSchema', () => { const defaultRequest = { uid: '1', avatar: { @@ -73,75 +52,29 @@ describe('User', () => { }; it('has expected attributes in request', () => { - const query = UserWithProfileInfoRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it.each(['initials', 'color', 'imageUrl'])('does not returns an error if %s is null', (key) => { - const reqWithNullImage = set(defaultRequest, `avatar.${key}`, null); - const query = UserWithProfileInfoRt.decode(reqWithNullImage); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: reqWithNullImage, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = UserWithProfileInfoRt.decode({ - ...defaultRequest, - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from avatar', () => { - const query = UserWithProfileInfoRt.decode({ - ...defaultRequest, - avatar: { ...defaultRequest.avatar, foo: 'bar' }, - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = UserWithProfileInfoSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it.each(['initials', 'color', 'imageUrl'])( - 'zod: does not return an error if %s is null', - (key) => { - const req = set( - { ...defaultRequest, avatar: { ...defaultRequest.avatar } }, - `avatar.${key}`, - null - ); - const result = UserWithProfileInfoSchema.safeParse(req); - expect(result.success).toBe(true); - expect(result.data).toStrictEqual(req); - } - ); + it.each(['initials', 'color', 'imageUrl'])('does not return an error if %s is null', (key) => { + const req = set( + { ...defaultRequest, avatar: { ...defaultRequest.avatar } }, + `avatar.${key}`, + null + ); + const result = UserWithProfileInfoSchema.safeParse(req); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(req); + }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = UserWithProfileInfoSchema.safeParse({ ...defaultRequest, foo: 'bar' }); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields from avatar', () => { + it('strips unknown fields from avatar', () => { const result = UserWithProfileInfoSchema.safeParse({ ...defaultRequest, avatar: { ...defaultRequest.avatar, foo: 'bar' }, @@ -151,7 +84,7 @@ describe('User', () => { }); }); - describe('UsersRt', () => { + describe('UsersSchema', () => { const defaultRequest = [ { email: 'reporter_no_uid@elastic.co', @@ -167,35 +100,12 @@ describe('User', () => { ]; it('has expected attributes in request', () => { - const query = UsersRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = UsersRt.decode([ - { - ...defaultRequest[0], - foo: 'bar', - }, - ]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [defaultRequest[0]], - }); - }); - - it('zod: has expected attributes in request', () => { const result = UsersSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = UsersSchema.safeParse([{ ...defaultRequest[0], foo: 'bar' }]); expect(result.success).toBe(true); expect(result.data).toStrictEqual([defaultRequest[0]]); @@ -203,35 +113,8 @@ describe('User', () => { }); describe('UserProfile', () => { - describe('CaseUserProfileRt', () => { + describe('CaseUserProfileSchema', () => { it('has expected attributes in response', () => { - const query = CaseUserProfileRt.decode({ - uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', - }, - }); - }); - - it('removes foo:bar attributes from response', () => { - const query = CaseUserProfileRt.decode({ - uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', - foo: 'bar', - }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', - }, - }); - }); - - it('zod: has expected attributes in response', () => { const result = CaseUserProfileSchema.safeParse({ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', }); @@ -241,7 +124,7 @@ describe('User', () => { }); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CaseUserProfileSchema.safeParse({ uid: 'u_J41Oh6L9ki-Vo2tOogS8WRTENzhHurGtRc87NgEAlkc_0', foo: 'bar', @@ -255,43 +138,16 @@ describe('User', () => { }); describe('Assignee', () => { - describe('CaseAssigneesRt', () => { + describe('CaseAssigneesSchema', () => { const defaultRequest = [{ uid: '1' }, { uid: '2' }]; it('has expected attributes in request', () => { - const query = CaseAssigneesRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = CaseAssigneesRt.decode([{ ...defaultRequest[0], foo: 'bar' }]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [defaultRequest[0]], - }); - }); - - it('removes foo:bar attributes from assignees', () => { - const query = CaseAssigneesRt.decode([{ uid: '1', foo: 'bar' }, { uid: '2' }]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [{ uid: '1' }, { uid: '2' }], - }); - }); - - it('zod: has expected attributes in request', () => { const result = CaseAssigneesSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = CaseAssigneesSchema.safeParse([{ uid: '1', foo: 'bar' }, { uid: '2' }]); expect(result.success).toBe(true); expect(result.data).toStrictEqual([{ uid: '1' }, { uid: '2' }]); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.ts index 824fdd35c7305..42077bf44aff9 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user/v1.ts @@ -5,50 +5,43 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; -const UserWithoutProfileUidRt = rt.strict({ - email: rt.union([rt.undefined, rt.null, rt.string]), - full_name: rt.union([rt.undefined, rt.null, rt.string]), - username: rt.union([rt.undefined, rt.null, rt.string]), +const UserWithoutProfileUidSchema = z.object({ + email: z.union([z.string(), z.null(), z.undefined()]), + full_name: z.union([z.string(), z.null(), z.undefined()]), + username: z.union([z.string(), z.null(), z.undefined()]), }); -export const UserRt = rt.intersection([ - UserWithoutProfileUidRt, - rt.exact(rt.partial({ profile_uid: rt.string })), -]); - -export const UserWithProfileInfoRt = rt.intersection([ - rt.strict({ - user: UserWithoutProfileUidRt, - }), - rt.exact(rt.partial({ uid: rt.string })), - rt.exact( - rt.partial({ - avatar: rt.exact( - rt.partial({ - initials: rt.union([rt.string, rt.null]), - color: rt.union([rt.string, rt.null]), - imageUrl: rt.union([rt.string, rt.null]), - }) - ), +export const UserSchema = UserWithoutProfileUidSchema.extend({ + profile_uid: z.string().optional(), +}); + +export const UserWithProfileInfoSchema = z.object({ + user: UserWithoutProfileUidSchema, + uid: z.string().optional(), + avatar: z + .object({ + initials: z.string().nullable().optional(), + color: z.string().nullable().optional(), + imageUrl: z.string().nullable().optional(), }) - ), -]); + .optional(), +}); -export const UsersRt = rt.array(UserRt); +export const UsersSchema = z.array(UserSchema); -export type User = rt.TypeOf; -export type UserWithProfileInfo = rt.TypeOf; +export type User = z.infer; +export type UserWithProfileInfo = z.infer; -export const CaseUserProfileRt = rt.strict({ - uid: rt.string, +export const CaseUserProfileSchema = z.object({ + uid: z.string(), }); -export type CaseUserProfile = rt.TypeOf; +export type CaseUserProfile = z.infer; /** * Assignees */ -export const CaseAssigneesRt = rt.array(CaseUserProfileRt); -export type CaseAssignees = rt.TypeOf; +export const CaseAssigneesSchema = z.array(CaseUserProfileSchema); +export type CaseAssignees = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts index bb86129935863..4b3499f2d749e 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/action/v1.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; /** * These values are used in a number of places including to define the accepted values in the @@ -48,9 +48,11 @@ export const UserActionActions = { push_to_service: 'push_to_service', } as const; -export const UserActionActionsRt = rt.keyof(UserActionActions); - /** * This defines the high level category for the user action. Whether the user add, removed, updated something */ -export type UserActionAction = rt.TypeOf; +export type UserActionAction = (typeof UserActionActions)[keyof typeof UserActionActions]; + +export const UserActionActionsSchema = z.enum( + Object.values(UserActionActions) as [UserActionAction, ...UserActionAction[]] +); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.test.ts index 8c6daf4d166d4..2e2cfee7c67be 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.test.ts @@ -6,33 +6,28 @@ */ import { UserActionTypes } from '../action/v1'; -import { AssigneesUserActionPayloadRt, AssigneesUserActionRt } from './v1'; +import { AssigneesUserActionPayloadSchema, AssigneesUserActionSchema } from './v1'; describe('Assignees', () => { - describe('AssigneesUserActionPayloadRt', () => { + describe('AssigneesUserActionPayloadSchema', () => { const defaultRequest = { assignees: [{ uid: '1' }, { uid: '2' }, { uid: '3' }], }; it('has expected attributes in request', () => { - const query = AssigneesUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = AssigneesUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = AssigneesUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = AssigneesUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('AssigneesUserActionRt', () => { + + describe('AssigneesUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.assignees, payload: { @@ -41,52 +36,42 @@ describe('Assignees', () => { }; it('has expected attributes in request', () => { - const query = AssigneesUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = AssigneesUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = AssigneesUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = AssigneesUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from assignees', () => { - const query = AssigneesUserActionRt.decode({ + it('strips unknown fields from assignees', () => { + const result = AssigneesUserActionSchema.safeParse({ type: UserActionTypes.assignees, payload: { assignees: [{ uid: '1', foo: 'bar' }, { uid: '2' }, { uid: '3' }], }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - type: UserActionTypes.assignees, - payload: { - assignees: [{ uid: '1' }, { uid: '2' }, { uid: '3' }], - }, + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ + type: UserActionTypes.assignees, + payload: { + assignees: [{ uid: '1' }, { uid: '2' }, { uid: '3' }], }, }); }); - it('removes foo:bar attributes from payload', () => { - const query = AssigneesUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = AssigneesUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.ts index f69c0a5efdbe7..27297c24331c3 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/assignees/v1.ts @@ -5,13 +5,13 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { CaseAssigneesRt } from '../../user/v1'; +import { z } from '@kbn/zod/v4'; +import { CaseAssigneesSchema } from '../../user/v1'; import { UserActionTypes } from '../action/v1'; -export const AssigneesUserActionPayloadRt = rt.strict({ assignees: CaseAssigneesRt }); +export const AssigneesUserActionPayloadSchema = z.object({ assignees: CaseAssigneesSchema }); -export const AssigneesUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.assignees), - payload: AssigneesUserActionPayloadRt, +export const AssigneesUserActionSchema = z.object({ + type: z.literal(UserActionTypes.assignees), + payload: AssigneesUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.test.ts index e3a528ddcb1bb..f95a28398391c 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.test.ts @@ -6,34 +6,28 @@ */ import { UserActionTypes } from '../action/v1'; -import { CategoryUserActionPayloadRt, CategoryUserActionRt } from './v1'; +import { CategoryUserActionPayloadSchema, CategoryUserActionSchema } from './v1'; describe('Category', () => { - describe('CategoryUserActionPayloadRt', () => { + describe('CategoryUserActionPayloadSchema', () => { const defaultRequest = { category: 'foobar', }; it('has expected attributes in request', () => { - const query = CategoryUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CategoryUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CategoryUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CategoryUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CategoryUserActionRt', () => { + describe('CategoryUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.category, payload: { @@ -42,33 +36,25 @@ describe('Category', () => { }; it('has expected attributes in request', () => { - const query = CategoryUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CategoryUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CategoryUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CategoryUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = CategoryUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = CategoryUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.ts index dea779a8ace3d..d485e94e386d0 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/category/v1.ts @@ -5,12 +5,14 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -export const CategoryUserActionPayloadRt = rt.strict({ category: rt.union([rt.string, rt.null]) }); +export const CategoryUserActionPayloadSchema = z.object({ + category: z.string().nullable(), +}); -export const CategoryUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.category), - payload: CategoryUserActionPayloadRt, +export const CategoryUserActionSchema = z.object({ + type: z.literal(UserActionTypes.category), + payload: CategoryUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.test.ts index edb7ab22b8d60..cd719c1cf905e 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.test.ts @@ -7,10 +7,10 @@ import { AttachmentType } from '../../attachment/v1'; import { UserActionTypes } from '../action/v1'; -import { CommentUserActionPayloadRt, CommentUserActionRt } from './v1'; +import { CommentUserActionPayloadSchema, CommentUserActionSchema } from './v1'; describe('Attachment', () => { - describe('CommentUserActionPayloadRt', () => { + describe('CommentUserActionPayloadSchema', () => { const defaultRequest = { comment: { comment: 'this is a sample comment', @@ -20,32 +20,24 @@ describe('Attachment', () => { }; it('has expected attributes in request', () => { - const query = CommentUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CommentUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CommentUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CommentUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from comment', () => { - const query = CommentUserActionPayloadRt.decode({ + it('strips unknown fields from comment', () => { + const result = CommentUserActionPayloadSchema.safeParse({ comment: { ...defaultRequest.comment, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts v2 unified format (comment payload with type and data)', () => { @@ -56,12 +48,12 @@ describe('Attachment', () => { owner: 'cases', }, }; - const query = CommentUserActionPayloadRt.decode(v2UnifiedRequest); + const result = CommentUserActionPayloadSchema.safeParse(v2UnifiedRequest); - expect(query._tag).toBe('Right'); - if (query._tag === 'Right') { - expect(query.right.comment).toHaveProperty('type', 'comment'); - expect(query.right.comment).toHaveProperty('data', { content: 'unified comment content' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.comment).toHaveProperty('type', 'comment'); + expect(result.data.comment).toHaveProperty('data', { content: 'unified comment content' }); } }); @@ -76,15 +68,16 @@ describe('Attachment', () => { }, }, }; - const query = CommentUserActionPayloadRt.decode(v2UnifiedRequest); + const result = CommentUserActionPayloadSchema.safeParse(v2UnifiedRequest); - expect(query._tag).toBe('Right'); - if (query._tag === 'Right') { - expect(query.right.comment).toEqual(v2UnifiedRequest.comment); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.comment).toEqual(v2UnifiedRequest.comment); } }); }); - describe('CommentUserActionRt', () => { + + describe('CommentUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.comment, payload: { @@ -97,33 +90,25 @@ describe('Attachment', () => { }; it('has expected attributes in request', () => { - const query = CommentUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CommentUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CommentUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CommentUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = CommentUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = CommentUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts v2 shape in payload', () => { @@ -137,13 +122,13 @@ describe('Attachment', () => { }, }, }; - const query = CommentUserActionRt.decode(v2PayloadRequest); + const result = CommentUserActionSchema.safeParse(v2PayloadRequest); - expect(query._tag).toBe('Right'); - if (query._tag === 'Right') { - expect(query.right.type).toBe(UserActionTypes.comment); - expect(query.right.payload.comment).toHaveProperty('type', 'comment'); - expect(query.right.payload.comment).toHaveProperty('data', { + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.type).toBe(UserActionTypes.comment); + expect(result.data.payload.comment).toHaveProperty('type', 'comment'); + expect(result.data.payload.comment).toHaveProperty('data', { content: 'v2 unified comment', }); } diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.ts index 582e216bbe820..345cb5508dd99 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/comment/v1.ts @@ -5,25 +5,25 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; import { - AttachmentRequestRtV2, - AttachmentRequestWithoutRefsRtV2, + AttachmentRequestSchemaV2, + AttachmentRequestWithoutRefsSchemaV2, } from '../../../api/attachment/v2'; -export const CommentUserActionPayloadRt = rt.strict({ comment: AttachmentRequestRtV2 }); +export const CommentUserActionPayloadSchema = z.object({ comment: AttachmentRequestSchemaV2 }); -export const CommentUserActionPayloadWithoutIdsRt = rt.strict({ - comment: AttachmentRequestWithoutRefsRtV2, +export const CommentUserActionPayloadWithoutIdsSchema = z.object({ + comment: AttachmentRequestWithoutRefsSchemaV2, }); -export const CommentUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.comment), - payload: CommentUserActionPayloadRt, +export const CommentUserActionSchema = z.object({ + type: z.literal(UserActionTypes.comment), + payload: CommentUserActionPayloadSchema, }); -export const CommentUserActionWithoutIdsRt = rt.strict({ - type: rt.literal(UserActionTypes.comment), - payload: CommentUserActionPayloadWithoutIdsRt, +export const CommentUserActionWithoutIdsSchema = z.object({ + type: z.literal(UserActionTypes.comment), + payload: CommentUserActionPayloadWithoutIdsSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.test.ts index 714eafec4ec1e..afa9b315bd0c1 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.test.ts @@ -6,10 +6,10 @@ */ import { ConnectorTypes } from '../../connector/v1'; -import { ConnectorUserActionPayloadWithoutConnectorIdRt } from './v1'; +import { ConnectorUserActionPayloadWithoutConnectorIdSchema } from './v1'; describe('Connector', () => { - describe('ConnectorUserActionPayloadWithoutConnectorIdRt', () => { + describe('ConnectorUserActionPayloadWithoutConnectorIdSchema', () => { const defaultRequest = { connector: { name: 'My JIRA connector', @@ -23,36 +23,27 @@ describe('Connector', () => { }; it('has expected attributes in request', () => { - const query = ConnectorUserActionPayloadWithoutConnectorIdRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = ConnectorUserActionPayloadWithoutConnectorIdSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = ConnectorUserActionPayloadWithoutConnectorIdRt.decode({ + it('strips unknown fields', () => { + const result = ConnectorUserActionPayloadWithoutConnectorIdSchema.safeParse({ ...defaultRequest, foo: 'bar', }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from fields', () => { - const query = ConnectorUserActionPayloadWithoutConnectorIdRt.decode({ + it('strips unknown fields from fields', () => { + const result = ConnectorUserActionPayloadWithoutConnectorIdSchema.safeParse({ ...defaultRequest, fields: { ...defaultRequest.connector.fields, foo: 'bar' }, }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.ts index 61c6e78f1f5ae..581e3a1160e29 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/connector/v1.ts @@ -5,24 +5,24 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { CaseUserActionConnectorRt, CaseConnectorRt } from '../../connector/v1'; +import { z } from '@kbn/zod/v4'; +import { CaseUserActionConnectorSchema, CaseConnectorSchema } from '../../connector/v1'; import { UserActionTypes } from '../action/v1'; -export const ConnectorUserActionPayloadWithoutConnectorIdRt = rt.strict({ - connector: CaseUserActionConnectorRt, +export const ConnectorUserActionPayloadWithoutConnectorIdSchema = z.object({ + connector: CaseUserActionConnectorSchema, }); -export const ConnectorUserActionPayloadRt = rt.strict({ - connector: CaseConnectorRt, +export const ConnectorUserActionPayloadSchema = z.object({ + connector: CaseConnectorSchema, }); -export const ConnectorUserActionWithoutConnectorIdRt = rt.strict({ - type: rt.literal(UserActionTypes.connector), - payload: ConnectorUserActionPayloadWithoutConnectorIdRt, +export const ConnectorUserActionWithoutConnectorIdSchema = z.object({ + type: z.literal(UserActionTypes.connector), + payload: ConnectorUserActionPayloadWithoutConnectorIdSchema, }); -export const ConnectorUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.connector), - payload: ConnectorUserActionPayloadRt, +export const ConnectorUserActionSchema = z.object({ + type: z.literal(UserActionTypes.connector), + payload: ConnectorUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.test.ts index 2fc49fc8f6eb4..7df5a0abedd7a 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.test.ts @@ -7,10 +7,10 @@ import { ConnectorTypes } from '../../connector/v1'; import { UserActionTypes } from '../action/v1'; -import { CreateCaseUserActionRt, CreateCaseUserActionWithoutConnectorIdRt } from './v1'; +import { CreateCaseUserActionSchema, CreateCaseUserActionWithoutConnectorIdSchema } from './v1'; describe('Create case', () => { - describe('CreateCaseUserActionRt', () => { + describe('CreateCaseUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.create_case, payload: { @@ -39,12 +39,9 @@ describe('Create case', () => { }; it('has expected attributes in request', () => { - const query = CreateCaseUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CreateCaseUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('empty category is decoded properly', () => { @@ -53,12 +50,9 @@ describe('Create case', () => { payload: { ...defaultRequest.payload, category: null }, }; - const query = CreateCaseUserActionRt.decode(defaultRequestEmptyCategory); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequestEmptyCategory, - }); + const result = CreateCaseUserActionSchema.safeParse(defaultRequestEmptyCategory); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequestEmptyCategory); }); it('string category is decoded properly', () => { @@ -67,12 +61,9 @@ describe('Create case', () => { payload: { ...defaultRequest.payload, category: 'sci-fi' }, }; - const query = CreateCaseUserActionRt.decode(defaultRequestStringCategory); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequestStringCategory, - }); + const result = CreateCaseUserActionSchema.safeParse(defaultRequestStringCategory); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequestStringCategory); }); it('customFields are decoded correctly', () => { @@ -94,37 +85,29 @@ describe('Create case', () => { payload: { ...defaultRequest.payload, customFields }, }; - const query = CreateCaseUserActionRt.decode(defaultRequestWithCustomFields); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequestWithCustomFields, - }); + const result = CreateCaseUserActionSchema.safeParse(defaultRequestWithCustomFields); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequestWithCustomFields); }); - it('removes foo:bar attributes from request', () => { - const query = CreateCaseUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CreateCaseUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = CreateCaseUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = CreateCaseUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CreateCaseUserActionWithoutConnectorIdRt', () => { + describe('CreateCaseUserActionWithoutConnectorIdSchema', () => { const defaultRequest = { type: UserActionTypes.create_case, payload: { @@ -152,12 +135,9 @@ describe('Create case', () => { }; it('has expected attributes in request', () => { - const query = CreateCaseUserActionWithoutConnectorIdRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CreateCaseUserActionWithoutConnectorIdSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('empty category in request', () => { @@ -166,12 +146,10 @@ describe('Create case', () => { payload: { ...defaultRequest.payload, category: null }, }; - const query = CreateCaseUserActionWithoutConnectorIdRt.decode(requestWithEmptyCategory); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithEmptyCategory, - }); + const result = + CreateCaseUserActionWithoutConnectorIdSchema.safeParse(requestWithEmptyCategory); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithEmptyCategory); }); it('string category in request', () => { @@ -180,12 +158,10 @@ describe('Create case', () => { payload: { ...defaultRequest.payload, category: 'romance' }, }; - const query = CreateCaseUserActionWithoutConnectorIdRt.decode(requestWithStringCategory); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: requestWithStringCategory, - }); + const result = + CreateCaseUserActionWithoutConnectorIdSchema.safeParse(requestWithStringCategory); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(requestWithStringCategory); }); it('customFields are decoded correctly', () => { @@ -207,36 +183,30 @@ describe('Create case', () => { payload: { ...defaultRequest.payload, customFields }, }; - const query = CreateCaseUserActionWithoutConnectorIdRt.decode(defaultRequestWithCustomFields); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequestWithCustomFields, - }); + const result = CreateCaseUserActionWithoutConnectorIdSchema.safeParse( + defaultRequestWithCustomFields + ); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequestWithCustomFields); }); - it('removes foo:bar attributes from request', () => { - const query = CreateCaseUserActionWithoutConnectorIdRt.decode({ + it('strips unknown fields', () => { + const result = CreateCaseUserActionWithoutConnectorIdSchema.safeParse({ ...defaultRequest, foo: 'bar', }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = CreateCaseUserActionWithoutConnectorIdRt.decode({ + it('strips unknown fields from payload', () => { + const result = CreateCaseUserActionWithoutConnectorIdSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.ts index 0a0938c9e4b2a..d4b9604df8843 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/create_case/v1.ts @@ -5,57 +5,39 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -import { AssigneesUserActionPayloadRt } from '../assignees/v1'; -import { CategoryUserActionPayloadRt } from '../category/v1'; +import { AssigneesUserActionPayloadSchema } from '../assignees/v1'; +import { CategoryUserActionPayloadSchema } from '../category/v1'; import { - ConnectorUserActionPayloadRt, - ConnectorUserActionPayloadWithoutConnectorIdRt, + ConnectorUserActionPayloadSchema, + ConnectorUserActionPayloadWithoutConnectorIdSchema, } from '../connector/v1'; -import { CustomFieldsUserActionPayloadRt } from '../custom_fields/v1'; -import { DescriptionUserActionPayloadRt } from '../description/v1'; -import { SettingsUserActionPayloadRt } from '../settings/v1'; -import { TagsUserActionPayloadRt } from '../tags/v1'; -import { TitleUserActionPayloadRt } from '../title/v1'; +import { CustomFieldsUserActionPayloadSchema } from '../custom_fields/v1'; +import { DescriptionUserActionPayloadSchema } from '../description/v1'; +import { SettingsUserActionPayloadSchema } from '../settings/v1'; +import { TagsUserActionPayloadSchema } from '../tags/v1'; +import { TitleUserActionPayloadSchema } from '../title/v1'; -const CommonFieldsRt = rt.strict({ - type: rt.literal(UserActionTypes.create_case), +const CommonPayloadAttributesSchema = z.object({ + assignees: AssigneesUserActionPayloadSchema.shape.assignees, + description: DescriptionUserActionPayloadSchema.shape.description, + status: z.string(), + severity: z.string(), + tags: TagsUserActionPayloadSchema.shape.tags, + title: TitleUserActionPayloadSchema.shape.title, + settings: SettingsUserActionPayloadSchema.shape.settings, + owner: z.string(), + category: CategoryUserActionPayloadSchema.shape.category.optional(), + customFields: CustomFieldsUserActionPayloadSchema.shape.customFields.optional(), }); -const CommonPayloadAttributesRt = rt.strict({ - assignees: AssigneesUserActionPayloadRt.type.props.assignees, - description: DescriptionUserActionPayloadRt.type.props.description, - status: rt.string, - severity: rt.string, - tags: TagsUserActionPayloadRt.type.props.tags, - title: TitleUserActionPayloadRt.type.props.title, - settings: SettingsUserActionPayloadRt.type.props.settings, - owner: rt.string, +export const CreateCaseUserActionSchema = z.object({ + type: z.literal(UserActionTypes.create_case), + payload: ConnectorUserActionPayloadSchema.merge(CommonPayloadAttributesSchema), }); -const OptionalPayloadAttributesRt = rt.exact( - rt.partial({ - category: CategoryUserActionPayloadRt.type.props.category, - customFields: CustomFieldsUserActionPayloadRt.type.props.customFields, - }) -); - -const PayloadAttributesRt = rt.intersection([ - CommonPayloadAttributesRt, - OptionalPayloadAttributesRt, -]); - -export const CreateCaseUserActionRt = rt.intersection([ - CommonFieldsRt, - rt.strict({ - payload: rt.intersection([ConnectorUserActionPayloadRt, PayloadAttributesRt]), - }), -]); - -export const CreateCaseUserActionWithoutConnectorIdRt = rt.intersection([ - CommonFieldsRt, - rt.strict({ - payload: rt.intersection([ConnectorUserActionPayloadWithoutConnectorIdRt, PayloadAttributesRt]), - }), -]); +export const CreateCaseUserActionWithoutConnectorIdSchema = z.object({ + type: z.literal(UserActionTypes.create_case), + payload: ConnectorUserActionPayloadWithoutConnectorIdSchema.merge(CommonPayloadAttributesSchema), +}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.test.ts index 00f8cdf747927..1537609e7c273 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.test.ts @@ -6,10 +6,10 @@ */ import { UserActionTypes } from '../action/v1'; -import { CustomFieldsUserActionPayloadRt, CustomFieldsUserActionRt } from './v1'; +import { CustomFieldsUserActionPayloadSchema, CustomFieldsUserActionSchema } from './v1'; describe('Custom field', () => { - describe('CustomFieldsUserActionPayloadRt', () => { + describe('CustomFieldsUserActionPayloadSchema', () => { const defaultRequest = { customFields: [ { @@ -21,25 +21,22 @@ describe('Custom field', () => { }; it('has expected attributes in request', () => { - const query = CustomFieldsUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CustomFieldsUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CustomFieldsUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields', () => { + const result = CustomFieldsUserActionPayloadSchema.safeParse({ + ...defaultRequest, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('CustomFieldsUserActionRt', () => { + describe('CustomFieldsUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.customFields, payload: { @@ -54,37 +51,29 @@ describe('Custom field', () => { }; it('has expected attributes in request', () => { - const query = CustomFieldsUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = CustomFieldsUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = CustomFieldsUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = CustomFieldsUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = CustomFieldsUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = CustomFieldsUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from the field', () => { - const query = CustomFieldsUserActionRt.decode({ + it('strips unknown fields from the field', () => { + const result = CustomFieldsUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, @@ -92,10 +81,8 @@ describe('Custom field', () => { }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.ts index 35a0377e03d60..98aa017766455 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/custom_fields/v1.ts @@ -5,15 +5,15 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { CaseCustomFieldsRt } from '../../custom_field/v1'; +import { z } from '@kbn/zod/v4'; +import { CaseCustomFieldsSchema } from '../../custom_field/v1'; import { UserActionTypes } from '../action/v1'; -export const CustomFieldsUserActionPayloadRt = rt.strict({ - customFields: CaseCustomFieldsRt, +export const CustomFieldsUserActionPayloadSchema = z.object({ + customFields: CaseCustomFieldsSchema, }); -export const CustomFieldsUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.customFields), - payload: CustomFieldsUserActionPayloadRt, +export const CustomFieldsUserActionSchema = z.object({ + type: z.literal(UserActionTypes.customFields), + payload: CustomFieldsUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.test.ts index 5568713043ca4..f75327e81ed1d 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.test.ts @@ -6,40 +6,35 @@ */ import { UserActionTypes } from '../action/v1'; -import { DeleteCaseUserActionRt } from './v1'; +import { DeleteCaseUserActionSchema } from './v1'; describe('Delete_case', () => { - describe('DeleteCaseUserActionRt', () => { + describe('DeleteCaseUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.delete_case, payload: {}, }; it('has expected attributes in request', () => { - const query = DeleteCaseUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = DeleteCaseUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = DeleteCaseUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = DeleteCaseUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = DeleteCaseUserActionRt.decode({ ...defaultRequest, payload: { foo: 'bar' } }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields from payload', () => { + const result = DeleteCaseUserActionSchema.safeParse({ + ...defaultRequest, + payload: { foo: 'bar' }, }); + + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.ts index 1e6a0bb3fde6a..b7b4dc6a24874 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/delete_case/v1.ts @@ -5,10 +5,10 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -export const DeleteCaseUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.delete_case), - payload: rt.strict({}), +export const DeleteCaseUserActionSchema = z.object({ + type: z.literal(UserActionTypes.delete_case), + payload: z.object({}), }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.test.ts index ef917193b459f..4a845be430e7b 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.test.ts @@ -6,34 +6,31 @@ */ import { UserActionTypes } from '../action/v1'; -import { DescriptionUserActionPayloadRt, DescriptionUserActionRt } from './v1'; +import { DescriptionUserActionPayloadSchema, DescriptionUserActionSchema } from './v1'; describe('Description', () => { - describe('DescriptionUserActionPayloadRt', () => { + describe('DescriptionUserActionPayloadSchema', () => { const defaultRequest = { description: 'this is sample description', }; it('has expected attributes in request', () => { - const query = DescriptionUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = DescriptionUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = DescriptionUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields', () => { + const result = DescriptionUserActionPayloadSchema.safeParse({ + ...defaultRequest, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('DescriptionUserActionRt', () => { + describe('DescriptionUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.description, payload: { @@ -42,33 +39,25 @@ describe('Description', () => { }; it('has expected attributes in request', () => { - const query = DescriptionUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = DescriptionUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = DescriptionUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = DescriptionUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = DescriptionUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = DescriptionUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.ts index 705e5d989db76..e5faab0f34bf6 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/description/v1.ts @@ -5,12 +5,12 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -export const DescriptionUserActionPayloadRt = rt.strict({ description: rt.string }); +export const DescriptionUserActionPayloadSchema = z.object({ description: z.string() }); -export const DescriptionUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.description), - payload: DescriptionUserActionPayloadRt, +export const DescriptionUserActionSchema = z.object({ + type: z.literal(UserActionTypes.description), + payload: DescriptionUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.test.ts index 05def186dd75d..6c11f1b9e9bbb 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.test.ts @@ -7,56 +7,53 @@ import { UserActionTypes } from '../action/v1'; import { - ExtendedFieldsRt, - ExtendedFieldsUserActionPayloadRt, - ExtendedFieldsUserActionRt, + ExtendedFieldsSchema, + ExtendedFieldsUserActionPayloadSchema, + ExtendedFieldsUserActionSchema, } from './v1'; describe('ExtendedFields', () => { - describe('ExtendedFieldsRt', () => { + describe('ExtendedFieldsSchema', () => { it('accepts a record of string to string', () => { - expect(ExtendedFieldsRt.decode({ risk_score: 'high', severity: 'medium' })).toStrictEqual({ - _tag: 'Right', - right: { risk_score: 'high', severity: 'medium' }, - }); + const result = ExtendedFieldsSchema.safeParse({ risk_score: 'high', severity: 'medium' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ risk_score: 'high', severity: 'medium' }); }); it('rejects a record with non-string values', () => { - const result = ExtendedFieldsRt.decode({ risk_score: 42 }); - expect(result._tag).toBe('Left'); + const result = ExtendedFieldsSchema.safeParse({ risk_score: 42 }); + expect(result.success).toBe(false); }); it('accepts an empty record', () => { - expect(ExtendedFieldsRt.decode({})).toStrictEqual({ - _tag: 'Right', - right: {}, - }); + const result = ExtendedFieldsSchema.safeParse({}); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({}); }); }); - describe('ExtendedFieldsUserActionPayloadRt', () => { + describe('ExtendedFieldsUserActionPayloadSchema', () => { const defaultRequest = { extended_fields: { risk_score: 'high' }, }; it('has expected attributes in request', () => { - expect(ExtendedFieldsUserActionPayloadRt.decode(defaultRequest)).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = ExtendedFieldsUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - expect( - ExtendedFieldsUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }) - ).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields', () => { + const result = ExtendedFieldsUserActionPayloadSchema.safeParse({ + ...defaultRequest, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('ExtendedFieldsUserActionRt', () => { + describe('ExtendedFieldsUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.extended_fields, payload: { @@ -65,29 +62,25 @@ describe('ExtendedFields', () => { }; it('has expected attributes in request', () => { - expect(ExtendedFieldsUserActionRt.decode(defaultRequest)).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = ExtendedFieldsUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - expect(ExtendedFieldsUserActionRt.decode({ ...defaultRequest, foo: 'bar' })).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = ExtendedFieldsUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - expect( - ExtendedFieldsUserActionRt.decode({ - ...defaultRequest, - payload: { ...defaultRequest.payload, foo: 'bar' }, - }) - ).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields from payload', () => { + const result = ExtendedFieldsUserActionSchema.safeParse({ + ...defaultRequest, + payload: { ...defaultRequest.payload, foo: 'bar' }, }); + + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.ts index 280e1ebe91d8f..95346add5c9d0 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/extended_fields/v1.ts @@ -5,16 +5,16 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -export const ExtendedFieldsRt = rt.record(rt.string, rt.string); +export const ExtendedFieldsSchema = z.record(z.string(), z.string()); -export const ExtendedFieldsUserActionPayloadRt = rt.strict({ - extended_fields: ExtendedFieldsRt, +export const ExtendedFieldsUserActionPayloadSchema = z.object({ + extended_fields: ExtendedFieldsSchema, }); -export const ExtendedFieldsUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.extended_fields), - payload: ExtendedFieldsUserActionPayloadRt, +export const ExtendedFieldsUserActionSchema = z.object({ + type: z.literal(UserActionTypes.extended_fields), + payload: ExtendedFieldsUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.test.ts index 8986e498db8fd..f0acc845e94a6 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.test.ts @@ -6,10 +6,10 @@ */ import { UserActionTypes } from '../action/v1'; -import { ObservablesUserActionPayloadRt, ObservablesUserActionRt } from './v1'; +import { ObservablesUserActionPayloadSchema, ObservablesUserActionSchema } from './v1'; describe('Observables', () => { - describe('ObservablesUserActionPayloadRt', () => { + describe('ObservablesUserActionPayloadSchema', () => { const defaultRequest = { observables: { count: 1, @@ -18,35 +18,31 @@ describe('Observables', () => { }; it('has expected attributes in request', () => { - const query = ObservablesUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = ObservablesUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = ObservablesUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields', () => { + const result = ObservablesUserActionPayloadSchema.safeParse({ + ...defaultRequest, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from observables', () => { - const query = ObservablesUserActionPayloadRt.decode({ + it('strips unknown fields from observables', () => { + const result = ObservablesUserActionPayloadSchema.safeParse({ observables: { ...defaultRequest.observables, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('ObservablesUserActionRt', () => { + + describe('ObservablesUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.observables, payload: { @@ -58,33 +54,25 @@ describe('Observables', () => { }; it('has expected attributes in request', () => { - const query = ObservablesUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = ObservablesUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = ObservablesUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = ObservablesUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = ObservablesUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = ObservablesUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.ts index 5c3c77dd99922..51f7e96abffa5 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/observables/v1.ts @@ -5,25 +5,27 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -const ObservablesActionTypeRt = rt.union([ - rt.literal('add'), - rt.literal('delete'), - rt.literal('update'), +const ObservablesActionTypeSchema = z.union([ + z.literal('add'), + z.literal('delete'), + z.literal('update'), ]); -export const ObservablePayloadRt = rt.strict({ - count: rt.number, - actionType: ObservablesActionTypeRt, +export const ObservablePayloadSchema = z.object({ + count: z.number(), + actionType: ObservablesActionTypeSchema, }); -export const ObservablesUserActionPayloadRt = rt.strict({ observables: ObservablePayloadRt }); +export const ObservablesUserActionPayloadSchema = z.object({ + observables: ObservablePayloadSchema, +}); -export const ObservablesUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.observables), - payload: ObservablesUserActionPayloadRt, +export const ObservablesUserActionSchema = z.object({ + type: z.literal(UserActionTypes.observables), + payload: ObservablesUserActionPayloadSchema, }); -export type ObservablesActionType = rt.TypeOf; +export type ObservablesActionType = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.test.ts index 1a8bb4a7f615a..4cd277fc1047d 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.test.ts @@ -7,14 +7,14 @@ import { UserActionTypes } from '../action/v1'; import { - PushedUserActionPayloadWithoutConnectorIdRt, - PushedUserActionPayloadRt, - PushedUserActionWithoutConnectorIdRt, - PushedUserActionRt, + PushedUserActionPayloadWithoutConnectorIdSchema, + PushedUserActionPayloadSchema, + PushedUserActionWithoutConnectorIdSchema, + PushedUserActionSchema, } from './v1'; describe('Pushed', () => { - describe('PushedUserActionPayloadWithoutConnectorIdRt', () => { + describe('PushedUserActionPayloadWithoutConnectorIdSchema', () => { const defaultRequest = { externalService: { connector_name: 'My SN connector', @@ -31,39 +31,31 @@ describe('Pushed', () => { }; it('has expected attributes in request', () => { - const query = PushedUserActionPayloadWithoutConnectorIdRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = PushedUserActionPayloadWithoutConnectorIdSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = PushedUserActionPayloadWithoutConnectorIdRt.decode({ + it('strips unknown fields', () => { + const result = PushedUserActionPayloadWithoutConnectorIdSchema.safeParse({ ...defaultRequest, foo: 'bar', }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from externalService', () => { - const query = PushedUserActionPayloadWithoutConnectorIdRt.decode({ + it('strips unknown fields from externalService', () => { + const result = PushedUserActionPayloadWithoutConnectorIdSchema.safeParse({ externalService: { ...defaultRequest.externalService, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('PushedUserActionPayloadRt', () => { + describe('PushedUserActionPayloadSchema', () => { const defaultRequest = { externalService: { connector_id: 'servicenow-1', @@ -81,36 +73,28 @@ describe('Pushed', () => { }; it('has expected attributes in request', () => { - const query = PushedUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = PushedUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = PushedUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = PushedUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from externalService', () => { - const query = PushedUserActionPayloadRt.decode({ + it('strips unknown fields from externalService', () => { + const result = PushedUserActionPayloadSchema.safeParse({ externalService: { ...defaultRequest.externalService, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('PushedUserActionWithoutConnectorIdRt', () => { + describe('PushedUserActionWithoutConnectorIdSchema', () => { const defaultRequest = { type: UserActionTypes.pushed, payload: { @@ -130,37 +114,32 @@ describe('Pushed', () => { }; it('has expected attributes in request', () => { - const query = PushedUserActionWithoutConnectorIdRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = PushedUserActionWithoutConnectorIdSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = PushedUserActionWithoutConnectorIdRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, + it('strips unknown fields', () => { + const result = PushedUserActionWithoutConnectorIdSchema.safeParse({ + ...defaultRequest, + foo: 'bar', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = PushedUserActionWithoutConnectorIdRt.decode({ + it('strips unknown fields from payload', () => { + const result = PushedUserActionWithoutConnectorIdSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('PushedUserActionRt', () => { + describe('PushedUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.pushed, payload: { @@ -181,33 +160,25 @@ describe('Pushed', () => { }; it('has expected attributes in request', () => { - const query = PushedUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = PushedUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = PushedUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = PushedUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from externalService', () => { - const query = PushedUserActionRt.decode({ + it('strips unknown fields from externalService', () => { + const result = PushedUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.ts index d8047ca0aee9c..1f4aea3fd7ade 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/pushed/v1.ts @@ -5,24 +5,24 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { ExternalServiceBasicRt, ExternalServiceRt } from '../../external_service/v1'; +import { z } from '@kbn/zod/v4'; +import { ExternalServiceBasicSchema, ExternalServiceSchema } from '../../external_service/v1'; import { UserActionTypes } from '../action/v1'; -export const PushedUserActionPayloadWithoutConnectorIdRt = rt.strict({ - externalService: ExternalServiceBasicRt, +export const PushedUserActionPayloadWithoutConnectorIdSchema = z.object({ + externalService: ExternalServiceBasicSchema, }); -export const PushedUserActionPayloadRt = rt.strict({ - externalService: ExternalServiceRt, +export const PushedUserActionPayloadSchema = z.object({ + externalService: ExternalServiceSchema, }); -export const PushedUserActionWithoutConnectorIdRt = rt.strict({ - type: rt.literal(UserActionTypes.pushed), - payload: PushedUserActionPayloadWithoutConnectorIdRt, +export const PushedUserActionWithoutConnectorIdSchema = z.object({ + type: z.literal(UserActionTypes.pushed), + payload: PushedUserActionPayloadWithoutConnectorIdSchema, }); -export const PushedUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.pushed), - payload: PushedUserActionPayloadRt, +export const PushedUserActionSchema = z.object({ + type: z.literal(UserActionTypes.pushed), + payload: PushedUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.test.ts index 4278393d38444..5dcc5abd79a6f 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.test.ts @@ -6,61 +6,52 @@ */ import { UserActionTypes } from '../action/v1'; -import { SettingsUserActionPayloadRt, SettingsUserActionRt } from './v1'; +import { SettingsUserActionPayloadSchema, SettingsUserActionSchema } from './v1'; describe('Settings', () => { - describe('SettingsUserActionPayloadRt', () => { + describe('SettingsUserActionPayloadSchema', () => { const defaultRequest = { settings: { syncAlerts: true, extractObservables: true }, }; it('has expected attributes in request', () => { - const query = SettingsUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = SettingsUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('has expected attributes in request with only syncAlerts', () => { - const query = SettingsUserActionPayloadRt.decode({ + const result = SettingsUserActionPayloadSchema.safeParse({ settings: { syncAlerts: true }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: { settings: { syncAlerts: true } }, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ settings: { syncAlerts: true } }); }); it('has expected attributes in request with only extractObservables', () => { - const query = SettingsUserActionPayloadRt.decode({ + const result = SettingsUserActionPayloadSchema.safeParse({ settings: { extractObservables: true }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: { settings: { extractObservables: true } }, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ settings: { extractObservables: true } }); }); - it('removes foo:bar attributes from request', () => { - const query = SettingsUserActionPayloadRt.decode({ + it('strips unknown fields', () => { + const result = SettingsUserActionPayloadSchema.safeParse({ settings: { syncAlerts: false, extractObservables: false }, foo: 'bar', }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - settings: { syncAlerts: false, extractObservables: false }, - }, + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ + settings: { syncAlerts: false, extractObservables: false }, }); }); }); - describe('SettingsUserActionRt', () => { + describe('SettingsUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.settings, payload: { @@ -69,63 +60,51 @@ describe('Settings', () => { }; it('has expected attributes in request', () => { - const query = SettingsUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = SettingsUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('has expected attributes in request with only syncAlerts', () => { - const query = SettingsUserActionRt.decode({ + const result = SettingsUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, settings: { syncAlerts: true } }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - payload: { ...defaultRequest.payload, settings: { syncAlerts: true } }, - }, + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ + ...defaultRequest, + payload: { ...defaultRequest.payload, settings: { syncAlerts: true } }, }); }); it('has expected attributes in request with only extractObservables', () => { - const query = SettingsUserActionRt.decode({ + const result = SettingsUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, settings: { extractObservables: true } }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: { - ...defaultRequest, - payload: { ...defaultRequest.payload, settings: { extractObservables: true } }, - }, + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ + ...defaultRequest, + payload: { ...defaultRequest.payload, settings: { extractObservables: true } }, }); }); - it('removes foo:bar attributes from request', () => { - const query = SettingsUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = SettingsUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = SettingsUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = SettingsUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.ts index 55a3772f9973f..da0ade181becc 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/settings/v1.ts @@ -5,18 +5,17 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -// Settings payload accepts one or both settings -export const SettingsUserActionPayloadRt = rt.strict({ - settings: rt.partial({ - syncAlerts: rt.boolean, - extractObservables: rt.boolean, +export const SettingsUserActionPayloadSchema = z.object({ + settings: z.object({ + syncAlerts: z.boolean().optional(), + extractObservables: z.boolean().optional(), }), }); -export const SettingsUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.settings), - payload: SettingsUserActionPayloadRt, +export const SettingsUserActionSchema = z.object({ + type: z.literal(UserActionTypes.settings), + payload: SettingsUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.test.ts index 0613840e90c08..3195415e61367 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.test.ts @@ -7,34 +7,28 @@ import { CaseSeverity } from '../../case/v1'; import { UserActionTypes } from '../action/v1'; -import { SeverityUserActionPayloadRt, SeverityUserActionRt } from './v1'; +import { SeverityUserActionPayloadSchema, SeverityUserActionSchema } from './v1'; describe('Severity', () => { - describe('SeverityUserActionPayloadRt', () => { + describe('SeverityUserActionPayloadSchema', () => { const defaultRequest = { severity: CaseSeverity.MEDIUM, }; it('has expected attributes in request', () => { - const query = SeverityUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = SeverityUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = SeverityUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = SeverityUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('SeverityUserActionRt', () => { + describe('SeverityUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.severity, payload: { @@ -43,33 +37,25 @@ describe('Severity', () => { }; it('has expected attributes in request', () => { - const query = SeverityUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = SeverityUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = SeverityUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = SeverityUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = SeverityUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = SeverityUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.ts index 03c69f195939b..f93d009e3cd57 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/severity/v1.ts @@ -5,13 +5,13 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { CaseSeverityRt } from '../../case/v1'; +import { z } from '@kbn/zod/v4'; +import { CaseSeveritySchema } from '../../case/v1'; import { UserActionTypes } from '../action/v1'; -export const SeverityUserActionPayloadRt = rt.strict({ severity: CaseSeverityRt }); +export const SeverityUserActionPayloadSchema = z.object({ severity: CaseSeveritySchema }); -export const SeverityUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.severity), - payload: SeverityUserActionPayloadRt, +export const SeverityUserActionSchema = z.object({ + type: z.literal(UserActionTypes.severity), + payload: SeverityUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.test.ts index 608027c78085e..0159241bd9c56 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.test.ts @@ -7,43 +7,38 @@ import { CaseStatuses } from '@kbn/cases-components'; import { UserActionTypes } from '../action/v1'; -import { StatusUserActionPayloadRt, StatusUserActionRt } from './v1'; +import { StatusUserActionPayloadSchema, StatusUserActionSchema } from './v1'; describe('Status', () => { - describe('StatusUserActionPayloadRt', () => { + describe('StatusUserActionPayloadSchema', () => { const defaultRequest = { status: CaseStatuses['in-progress'], }; it('has expected attributes in request', () => { - const query = StatusUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = StatusUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = StatusUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = StatusUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); it('accepts syncedAlertCount when provided', () => { - const query = StatusUserActionPayloadRt.decode({ ...defaultRequest, syncedAlertCount: 3 }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: { ...defaultRequest, syncedAlertCount: 3 }, + const result = StatusUserActionPayloadSchema.safeParse({ + ...defaultRequest, + syncedAlertCount: 3, }); + + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ ...defaultRequest, syncedAlertCount: 3 }); }); }); - describe('StatusUserActionRt', () => { + describe('StatusUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.status, payload: { @@ -52,33 +47,25 @@ describe('Status', () => { }; it('has expected attributes in request', () => { - const query = StatusUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = StatusUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = StatusUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = StatusUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = StatusUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = StatusUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.ts index 5b726d09e1d3e..3b9f7cedf212d 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/status/v1.ts @@ -5,18 +5,17 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { CaseStatusRt, CaseCloseReasonRt } from '../../case/v1'; +import { z } from '@kbn/zod/v4'; +import { CaseCloseReasonSchema, CaseStatusSchema } from '../../case/v1'; import { UserActionTypes } from '../action/v1'; -export const StatusUserActionPayloadRt = rt.exact( - rt.intersection([ - rt.type({ status: CaseStatusRt }), - rt.partial({ closeReason: CaseCloseReasonRt, syncedAlertCount: rt.number }), - ]) -); +export const StatusUserActionPayloadSchema = z.object({ + status: CaseStatusSchema, + closeReason: CaseCloseReasonSchema.optional(), + syncedAlertCount: z.number().optional(), +}); -export const StatusUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.status), - payload: StatusUserActionPayloadRt, +export const StatusUserActionSchema = z.object({ + type: z.literal(UserActionTypes.status), + payload: StatusUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.test.ts index ef6a45a46b2fe..20923e620fb3e 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.test.ts @@ -6,34 +6,28 @@ */ import { UserActionTypes } from '../action/v1'; -import { TagsUserActionPayloadRt, TagsUserActionRt } from './v1'; +import { TagsUserActionPayloadSchema, TagsUserActionSchema } from './v1'; describe('Tags', () => { - describe('TagsUserActionPayloadRt', () => { + describe('TagsUserActionPayloadSchema', () => { const defaultRequest = { tags: ['one', 'two'], }; it('has expected attributes in request', () => { - const query = TagsUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = TagsUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = TagsUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = TagsUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('TagsUserActionRt', () => { + describe('TagsUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.tags, payload: { @@ -42,33 +36,25 @@ describe('Tags', () => { }; it('has expected attributes in request', () => { - const query = TagsUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = TagsUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = TagsUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = TagsUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = TagsUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = TagsUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.ts index 9867c8d2341e7..f23666bfb27c7 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/tags/v1.ts @@ -5,12 +5,12 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -export const TagsUserActionPayloadRt = rt.strict({ tags: rt.array(rt.string) }); +export const TagsUserActionPayloadSchema = z.object({ tags: z.array(z.string()) }); -export const TagsUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.tags), - payload: TagsUserActionPayloadRt, +export const TagsUserActionSchema = z.object({ + type: z.literal(UserActionTypes.tags), + payload: TagsUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.test.ts index f252a4e4b2225..b021a841fbfea 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.test.ts @@ -6,41 +6,39 @@ */ import { UserActionTypes } from '../action/v1'; -import { TemplateUserActionPayloadRt, TemplateUserActionRt } from './v1'; +import { TemplateUserActionPayloadSchema, TemplateUserActionSchema } from './v1'; -describe('TemplateUserActionPayloadRt', () => { +describe('TemplateUserActionPayloadSchema', () => { it('accepts a payload with a template object', () => { - expect( - TemplateUserActionPayloadRt.decode({ template: { id: 'tmpl-1', version: 3 } }) - ).toStrictEqual({ - _tag: 'Right', - right: { template: { id: 'tmpl-1', version: 3 } }, + const result = TemplateUserActionPayloadSchema.safeParse({ + template: { id: 'tmpl-1', version: 3 }, }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ template: { id: 'tmpl-1', version: 3 } }); }); it('accepts a payload with null template (remove)', () => { - expect(TemplateUserActionPayloadRt.decode({ template: null })).toStrictEqual({ - _tag: 'Right', - right: { template: null }, - }); + const result = TemplateUserActionPayloadSchema.safeParse({ template: null }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ template: null }); }); it('rejects a payload with a missing template field', () => { - const result = TemplateUserActionPayloadRt.decode({}); - expect(result._tag).toBe('Left'); + const result = TemplateUserActionPayloadSchema.safeParse({}); + expect(result.success).toBe(false); }); it('strips extra fields from the payload', () => { - expect( - TemplateUserActionPayloadRt.decode({ template: { id: 'tmpl-1', version: 3 }, extra: 'drop' }) - ).toStrictEqual({ - _tag: 'Right', - right: { template: { id: 'tmpl-1', version: 3 } }, + const result = TemplateUserActionPayloadSchema.safeParse({ + template: { id: 'tmpl-1', version: 3 }, + extra: 'drop', }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual({ template: { id: 'tmpl-1', version: 3 } }); }); }); -describe('TemplateUserActionRt', () => { +describe('TemplateUserActionSchema', () => { const validApplyRequest = { type: UserActionTypes.template, payload: { template: { id: 'tmpl-1', version: 3 } }, @@ -52,40 +50,34 @@ describe('TemplateUserActionRt', () => { }; it('accepts an apply-template action', () => { - expect(TemplateUserActionRt.decode(validApplyRequest)).toStrictEqual({ - _tag: 'Right', - right: validApplyRequest, - }); + const result = TemplateUserActionSchema.safeParse(validApplyRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(validApplyRequest); }); it('accepts a remove-template action (null payload)', () => { - expect(TemplateUserActionRt.decode(validRemoveRequest)).toStrictEqual({ - _tag: 'Right', - right: validRemoveRequest, - }); + const result = TemplateUserActionSchema.safeParse(validRemoveRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(validRemoveRequest); }); it('strips extra top-level fields', () => { - expect(TemplateUserActionRt.decode({ ...validApplyRequest, extra: 'drop' })).toStrictEqual({ - _tag: 'Right', - right: validApplyRequest, - }); + const result = TemplateUserActionSchema.safeParse({ ...validApplyRequest, extra: 'drop' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(validApplyRequest); }); it('strips extra fields from payload', () => { - expect( - TemplateUserActionRt.decode({ - ...validApplyRequest, - payload: { ...validApplyRequest.payload, extra: 'drop' }, - }) - ).toStrictEqual({ - _tag: 'Right', - right: validApplyRequest, + const result = TemplateUserActionSchema.safeParse({ + ...validApplyRequest, + payload: { ...validApplyRequest.payload, extra: 'drop' }, }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(validApplyRequest); }); it('rejects a wrong type', () => { - const result = TemplateUserActionRt.decode({ ...validApplyRequest, type: 'title' }); - expect(result._tag).toBe('Left'); + const result = TemplateUserActionSchema.safeParse({ ...validApplyRequest, type: 'title' }); + expect(result.success).toBe(false); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.ts index 4d213a1ec404a..ed5e992a09f69 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/template/v1.ts @@ -5,20 +5,19 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -export const TemplateUserActionPayloadRt = rt.strict({ - template: rt.union([ - rt.strict({ - id: rt.string, - version: rt.number, - }), - rt.null, - ]), +export const TemplateUserActionPayloadSchema = z.object({ + template: z + .object({ + id: z.string(), + version: z.number(), + }) + .nullable(), }); -export const TemplateUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.template), - payload: TemplateUserActionPayloadRt, +export const TemplateUserActionSchema = z.object({ + type: z.literal(UserActionTypes.template), + payload: TemplateUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.test.ts index 3f27de84b74da..fec451ba61f2d 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.test.ts @@ -6,34 +6,28 @@ */ import { UserActionTypes } from '../action/v1'; -import { TitleUserActionPayloadRt, TitleUserActionRt } from './v1'; +import { TitleUserActionPayloadSchema, TitleUserActionSchema } from './v1'; describe('Title', () => { - describe('TitleUserActionPayloadRt', () => { + describe('TitleUserActionPayloadSchema', () => { const defaultRequest = { title: 'sample title', }; it('has expected attributes in request', () => { - const query = TitleUserActionPayloadRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = TitleUserActionPayloadSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = TitleUserActionPayloadRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = TitleUserActionPayloadSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); - describe('TitleUserActionRt', () => { + describe('TitleUserActionSchema', () => { const defaultRequest = { type: UserActionTypes.title, payload: { @@ -42,33 +36,25 @@ describe('Title', () => { }; it('has expected attributes in request', () => { - const query = TitleUserActionRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + const result = TitleUserActionSchema.safeParse(defaultRequest); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from request', () => { - const query = TitleUserActionRt.decode({ ...defaultRequest, foo: 'bar' }); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + it('strips unknown fields', () => { + const result = TitleUserActionSchema.safeParse({ ...defaultRequest, foo: 'bar' }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); - it('removes foo:bar attributes from payload', () => { - const query = TitleUserActionRt.decode({ + it('strips unknown fields from payload', () => { + const result = TitleUserActionSchema.safeParse({ ...defaultRequest, payload: { ...defaultRequest.payload, foo: 'bar' }, }); - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); + expect(result.success).toBe(true); + expect(result.data).toStrictEqual(defaultRequest); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.ts index 0a2fe999d250d..d893b61a045c0 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/title/v1.ts @@ -5,12 +5,12 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { UserActionTypes } from '../action/v1'; -export const TitleUserActionPayloadRt = rt.strict({ title: rt.string }); +export const TitleUserActionPayloadSchema = z.object({ title: z.string() }); -export const TitleUserActionRt = rt.strict({ - type: rt.literal(UserActionTypes.title), - payload: TitleUserActionPayloadRt, +export const TitleUserActionSchema = z.object({ + type: z.literal(UserActionTypes.title), + payload: TitleUserActionPayloadSchema, }); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.test.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.test.ts index 1a401a8bf9b0f..9523b1dd90128 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.test.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.test.ts @@ -6,12 +6,10 @@ */ import { AttachmentType } from '../attachment/v1'; -import { UserActionTypes } from './action/v1'; -import { UserActionsRt } from './v1'; -import { UserActionsSchema } from '../../domain_zod/user_action/v1'; +import { UserActionTypes, UserActionsSchema } from './v1'; describe('User actions', () => { - describe('UserActionsRt', () => { + describe('UserActionsSchema', () => { const defaultRequest = [ { type: UserActionTypes.comment, @@ -37,41 +35,12 @@ describe('User actions', () => { ]; it('has expected attributes in request', () => { - const query = UserActionsRt.decode(defaultRequest); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: [...defaultRequest], - }); - }); - - it('removes foo:bar attributes from request', () => { - const query = UserActionsRt.decode([{ ...defaultRequest[0], foo: 'bar' }]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('removes foo:bar attributes from payload', () => { - const query = UserActionsRt.decode([ - { ...defaultRequest[0], payload: { ...defaultRequest[0].payload, foo: 'bar' } }, - ]); - - expect(query).toStrictEqual({ - _tag: 'Right', - right: defaultRequest, - }); - }); - - it('zod: has expected attributes in request', () => { const result = UserActionsSchema.safeParse(defaultRequest); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); }); - it('zod: strips unknown fields', () => { + it('strips unknown fields', () => { const result = UserActionsSchema.safeParse([{ ...defaultRequest[0], foo: 'bar' }]); expect(result.success).toBe(true); expect(result.data).toStrictEqual(defaultRequest); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts index e53ae527db19b..c2d75a91cf36d 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain/user_action/v1.ts @@ -5,128 +5,129 @@ * 2.0. */ -import * as rt from 'io-ts'; -import { UserRt } from '../user/v1'; -import { UserActionActionsRt } from './action/v1'; -import { AssigneesUserActionRt } from './assignees/v1'; -import { CategoryUserActionRt } from './category/v1'; -import type { CommentUserActionPayloadWithoutIdsRt } from './comment/v1'; -import { CommentUserActionRt, CommentUserActionWithoutIdsRt } from './comment/v1'; -import { ConnectorUserActionRt, ConnectorUserActionWithoutConnectorIdRt } from './connector/v1'; -import { CreateCaseUserActionRt, CreateCaseUserActionWithoutConnectorIdRt } from './create_case/v1'; -import { DeleteCaseUserActionRt } from './delete_case/v1'; -import { DescriptionUserActionRt } from './description/v1'; -import { PushedUserActionRt, PushedUserActionWithoutConnectorIdRt } from './pushed/v1'; -import type { SettingsUserActionPayloadRt } from './settings/v1'; -import { SettingsUserActionRt } from './settings/v1'; -import { SeverityUserActionRt } from './severity/v1'; -import { StatusUserActionRt } from './status/v1'; -import { TagsUserActionRt } from './tags/v1'; -import { TitleUserActionRt } from './title/v1'; -import { CustomFieldsUserActionRt } from './custom_fields/v1'; -import { ObservablesUserActionRt } from './observables/v1'; -import { ExtendedFieldsUserActionRt } from './extended_fields/v1'; -import { TemplateUserActionRt } from './template/v1'; -export { UserActionTypes, UserActionActions } from './action/v1'; -export { StatusUserActionRt } from './status/v1'; +import { z } from '@kbn/zod/v4'; +import { UserSchema } from '../user/v1'; +import { UserActionActionsSchema } from './action/v1'; +import { AssigneesUserActionSchema } from './assignees/v1'; +import { CategoryUserActionSchema } from './category/v1'; +import type { CommentUserActionPayloadWithoutIdsSchema } from './comment/v1'; +import { CommentUserActionSchema, CommentUserActionWithoutIdsSchema } from './comment/v1'; +import { + ConnectorUserActionSchema, + ConnectorUserActionWithoutConnectorIdSchema, +} from './connector/v1'; +import { + CreateCaseUserActionSchema, + CreateCaseUserActionWithoutConnectorIdSchema, +} from './create_case/v1'; +import { DeleteCaseUserActionSchema } from './delete_case/v1'; +import { DescriptionUserActionSchema } from './description/v1'; +import { PushedUserActionSchema, PushedUserActionWithoutConnectorIdSchema } from './pushed/v1'; +import type { SettingsUserActionPayloadSchema } from './settings/v1'; +import { SettingsUserActionSchema } from './settings/v1'; +import { SeverityUserActionSchema } from './severity/v1'; +import { StatusUserActionSchema } from './status/v1'; +import { TagsUserActionSchema } from './tags/v1'; +import { TitleUserActionSchema } from './title/v1'; +import { CustomFieldsUserActionSchema } from './custom_fields/v1'; +import { ObservablesUserActionSchema } from './observables/v1'; +import { TemplateUserActionSchema } from './template/v1'; +import { ExtendedFieldsUserActionSchema } from './extended_fields/v1'; +export { UserActionTypes, UserActionActions } from './action/v1'; +export { StatusUserActionSchema } from './status/v1'; export type { UserActionType, UserActionAction } from './action/v1'; -const UserActionCommonAttributesRt = rt.strict({ - created_at: rt.string, - created_by: UserRt, - owner: rt.string, - action: UserActionActionsRt, +const UserActionCommonAttributesSchema = z.object({ + created_at: z.string(), + created_by: UserSchema, + owner: z.string(), + action: UserActionActionsSchema, }); /** * This should only be used for the getAll route and it should be removed when the route is removed - * @deprecated use CaseUserActionInjectedIdsRt instead + * @deprecated use CaseUserActionInjectedIdsSchema instead */ -export const CaseUserActionInjectedDeprecatedIdsRt = rt.strict({ - action_id: rt.string, - case_id: rt.string, - comment_id: rt.union([rt.string, rt.null]), +export const CaseUserActionInjectedDeprecatedIdsSchema = z.object({ + action_id: z.string(), + case_id: z.string(), + comment_id: z.string().nullable(), }); -export const CaseUserActionInjectedIdsRt = rt.strict({ - comment_id: rt.union([rt.string, rt.null]), +export const CaseUserActionInjectedIdsSchema = z.object({ + comment_id: z.string().nullable(), }); -const BasicUserActionsRt = rt.union([ - DescriptionUserActionRt, - TagsUserActionRt, - TitleUserActionRt, - SettingsUserActionRt, - StatusUserActionRt, - SeverityUserActionRt, - AssigneesUserActionRt, - DeleteCaseUserActionRt, - CategoryUserActionRt, - CustomFieldsUserActionRt, - ObservablesUserActionRt, - ExtendedFieldsUserActionRt, - TemplateUserActionRt, +const BasicUserActionsSchema = z.union([ + DescriptionUserActionSchema, + TagsUserActionSchema, + TitleUserActionSchema, + SettingsUserActionSchema, + StatusUserActionSchema, + SeverityUserActionSchema, + AssigneesUserActionSchema, + DeleteCaseUserActionSchema, + CategoryUserActionSchema, + CustomFieldsUserActionSchema, + ObservablesUserActionSchema, + ExtendedFieldsUserActionSchema, + TemplateUserActionSchema, ]); -const CommonUserActionsWithIdsRt = rt.union([BasicUserActionsRt, CommentUserActionRt]); -const CommonUserActionsWithoutIdsRt = rt.union([BasicUserActionsRt, CommentUserActionWithoutIdsRt]); - -const UserActionPayloadRt = rt.union([ - CommonUserActionsWithIdsRt, - CreateCaseUserActionRt, - ConnectorUserActionRt, - PushedUserActionRt, +const CommonUserActionsWithIdsSchema = z.union([BasicUserActionsSchema, CommentUserActionSchema]); +const CommonUserActionsWithoutIdsSchema = z.union([ + BasicUserActionsSchema, + CommentUserActionWithoutIdsSchema, ]); -const UserActionsWithoutIdsRt = rt.union([ - CommonUserActionsWithoutIdsRt, - CreateCaseUserActionWithoutConnectorIdRt, - ConnectorUserActionWithoutConnectorIdRt, - PushedUserActionWithoutConnectorIdRt, +const UserActionPayloadSchema = z.union([ + CommonUserActionsWithIdsSchema, + CreateCaseUserActionSchema, + ConnectorUserActionSchema, + PushedUserActionSchema, ]); -export const CaseUserActionBasicRt = rt.intersection([ - UserActionPayloadRt, - UserActionCommonAttributesRt, +const UserActionsWithoutIdsSchema = z.union([ + CommonUserActionsWithoutIdsSchema, + CreateCaseUserActionWithoutConnectorIdSchema, + ConnectorUserActionWithoutConnectorIdSchema, + PushedUserActionWithoutConnectorIdSchema, ]); -export const CaseUserActionWithoutReferenceIdsRt = rt.intersection([ - UserActionsWithoutIdsRt, - UserActionCommonAttributesRt, -]); +export const CaseUserActionBasicSchema = UserActionPayloadSchema.and( + UserActionCommonAttributesSchema +); + +export const CaseUserActionWithoutReferenceIdsSchema = UserActionsWithoutIdsSchema.and( + UserActionCommonAttributesSchema +); /** * This includes the comment_id but not the action_id or case_id */ -export const UserActionAttributesRt = rt.intersection([ - CaseUserActionBasicRt, - CaseUserActionInjectedIdsRt, -]); +export const UserActionAttributesSchema = CaseUserActionBasicSchema.and( + CaseUserActionInjectedIdsSchema +); -const UserActionRt = rt.intersection([ - UserActionAttributesRt, - rt.strict({ - id: rt.string, - version: rt.string, - }), -]); +const UserActionSchema = UserActionAttributesSchema.and( + z.object({ id: z.string(), version: z.string() }) +); -export const UserActionsRt = rt.array(UserActionRt); +export const UserActionsSchema = z.array(UserActionSchema); -type UserActionWithAttributes = T & rt.TypeOf; -export type UserActionWithDeprecatedResponse = T & - rt.TypeOf; - -export type CaseUserActionWithoutReferenceIds = rt.TypeOf< - typeof CaseUserActionWithoutReferenceIdsRt +export type CaseUserActionWithoutReferenceIds = z.infer< + typeof CaseUserActionWithoutReferenceIdsSchema >; +export type UserActionPayload = z.infer; +export type UserActionAttributes = z.infer; +export type UserActions = z.infer; -export type UserActionPayload = rt.TypeOf; -export type UserActionAttributes = rt.TypeOf; -export type UserActions = rt.TypeOf; +type UserActionWithAttributes = T & z.infer; +export type UserActionWithDeprecatedResponse = T & + z.infer; export type UserAction = Omit< - rt.TypeOf, + z.infer, 'type' | 'payload' > & T; @@ -134,34 +135,33 @@ export type UserAction = Omit< /** * User actions */ -export type AssigneesUserAction = UserAction>; -export type CategoryUserAction = UserAction>; -export type CommentUserAction = UserAction>; +export type AssigneesUserAction = UserAction>; +export type CategoryUserAction = UserAction>; +export type CommentUserAction = UserAction>; export type CommentUserActionPayloadWithoutIds = UserActionWithAttributes< - rt.TypeOf + z.infer >; -export type ConnectorUserAction = UserAction>; +export type ConnectorUserAction = UserAction>; export type ConnectorUserActionWithoutConnectorId = UserActionWithAttributes< - rt.TypeOf + z.infer >; -export type DeleteCaseUserAction = UserAction>; -export type DescriptionUserAction = UserAction>; -export type PushedUserAction = UserAction>; +export type DeleteCaseUserAction = UserAction>; +export type DescriptionUserAction = UserAction>; +export type PushedUserAction = UserAction>; export type PushedUserActionWithoutConnectorId = UserActionWithAttributes< - rt.TypeOf + z.infer >; -export type SettingsUserAction = UserAction>; -export type SettingsUserActionPayload = rt.TypeOf; -export type SeverityUserAction = UserAction>; -export type StatusUserAction = UserAction>; -export type TagsUserAction = UserAction>; -export type TitleUserAction = UserAction>; -export type CreateCaseUserAction = UserAction>; +export type SettingsUserAction = UserAction>; +export type SettingsUserActionPayload = z.infer; +export type SeverityUserAction = UserAction>; +export type StatusUserAction = UserAction>; +export type TagsUserAction = UserAction>; +export type TitleUserAction = UserAction>; +export type CreateCaseUserAction = UserAction>; export type CreateCaseUserActionWithoutConnectorId = UserActionWithAttributes< - rt.TypeOf + z.infer >; -export type CustomFieldsUserAction = UserAction>; -export type ObservablesUserAction = UserAction>; -export type ExtendedFieldsUserAction = UserAction>; -export { ExtendedFieldsRt } from './extended_fields/v1'; -export type TemplateUserAction = UserAction>; +export type CustomFieldsUserAction = UserAction>; +export type ObservablesUserAction = UserAction>; +export type ExtendedFieldsUserAction = UserAction>; +export type TemplateUserAction = UserAction>; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/file/v2.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/file/v2.ts index 38197a37697e5..fb4a5462aa970 100644 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/file/v2.ts +++ b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/file/v2.ts @@ -8,7 +8,7 @@ import { z } from '@kbn/zod/v4'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { FILE_ATTACHMENT_TYPE } from '../../../../constants/attachments'; -import { SingleFileAttachmentMetadataSchema } from '../v1'; +import { SingleFileAttachmentMetadataSchema } from '../../../domain/attachment/v1'; export const FileAttachmentMetadataSchema = z .object({ diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/v1.ts deleted file mode 100644 index 1f10e50a68f55..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/v1.ts +++ /dev/null @@ -1,332 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { limitedStringSchema, mimeTypeString, jsonValueSchema } from '../../../schema_zod'; -import { UserSchema } from '../user/v1'; -import { MAX_FILENAME_LENGTH } from '../../../constants'; -import { AttachmentType, ExternalReferenceStorageType } from '../../domain/attachment/v1'; - -export { AttachmentType, ExternalReferenceStorageType }; - -/** - * Files - */ -export const SingleFileAttachmentMetadataSchema = z.object({ - name: z.string(), - extension: z.string(), - mimeType: z.string(), - created: z.string(), -}); - -export const FileAttachmentMetadataSchema = z.object({ - files: z.array(SingleFileAttachmentMetadataSchema), -}); - -export type FileAttachmentMetadata = z.infer; - -export const AttachmentAttributesBasicSchema = z.object({ - created_at: z.string(), - created_by: UserSchema, - owner: z.string(), - pushed_at: z.string().nullable(), - pushed_by: UserSchema.nullable(), - updated_at: z.string().nullable(), - updated_by: UserSchema.nullable(), -}); - -export const FileAttachmentMetadataPayloadSchema = z.object({ - mimeType: mimeTypeString, - filename: limitedStringSchema({ fieldName: 'filename', min: 1, max: MAX_FILENAME_LENGTH }), -}); - -/** - * User comment - */ -export const UserCommentAttachmentPayloadSchema = z.object({ - comment: z.string(), - type: z.literal(AttachmentType.user), - owner: z.string(), -}); - -const UserCommentAttachmentAttributesSchema = UserCommentAttachmentPayloadSchema.merge( - AttachmentAttributesBasicSchema -); - -export const UserCommentAttachmentSchema = UserCommentAttachmentAttributesSchema.extend({ - id: z.string(), - version: z.string(), -}); - -export type UserCommentAttachmentPayload = z.infer; -export type UserCommentAttachmentAttributes = z.infer; -export type UserCommentAttachment = z.infer; - -/** - * Generic event - */ -export const EventAttachmentPayloadSchema = z.object({ - type: z.literal(AttachmentType.event), - eventId: z.union([z.array(z.string()), z.string()]), - index: z.union([z.array(z.string()), z.string()]), - owner: z.string(), -}); - -/** - * Alerts - */ -export const AlertAttachmentPayloadSchema = z.object({ - type: z.literal(AttachmentType.alert), - alertId: z.union([z.array(z.string()), z.string()]), - index: z.union([z.array(z.string()), z.string()]), - rule: z.object({ - id: z.string().nullable(), - name: z.string().nullable(), - }), - owner: z.string(), -}); - -export const AlertAttachmentAttributesSchema = AlertAttachmentPayloadSchema.merge( - AttachmentAttributesBasicSchema -); - -export const EventAttachmentAttributesSchema = EventAttachmentPayloadSchema.merge( - AttachmentAttributesBasicSchema -); - -export const AlertAttachmentSchema = AlertAttachmentAttributesSchema.extend({ - id: z.string(), - version: z.string(), -}); - -export const EventAttachmentSchema = EventAttachmentAttributesSchema.extend({ - id: z.string(), - version: z.string(), -}); - -export type AlertAttachmentPayload = z.infer; -export type AlertAttachmentAttributes = z.infer; -export type AlertAttachment = z.infer; - -export type EventAttachmentPayload = z.infer; -export type EventAttachmentAttributes = z.infer; -export type EventAttachment = z.infer; - -export const DocumentAttachmentAttributesSchema = z.union([ - AlertAttachmentAttributesSchema, - EventAttachmentAttributesSchema, -]); -export type DocumentAttachmentAttributes = z.infer; - -/** - * Actions - */ -export const ActionsAttachmentPayloadSchema = z.object({ - type: z.literal(AttachmentType.actions), - comment: z.string(), - actions: z.object({ - targets: z.array( - z.object({ - hostname: z.string(), - endpointId: z.string(), - }) - ), - type: z.string(), - }), - owner: z.string(), -}); - -const ActionsAttachmentAttributesSchema = ActionsAttachmentPayloadSchema.merge( - AttachmentAttributesBasicSchema -); - -export const ActionsAttachmentSchema = ActionsAttachmentAttributesSchema.extend({ - id: z.string(), - version: z.string(), -}); - -export type ActionsAttachmentPayload = z.infer; -export type ActionsAttachmentAttributes = z.infer; -export type ActionsAttachment = z.infer; - -/** - * External reference - */ -const ExternalReferenceStorageNoSOSchema = z.object({ - type: z.literal(ExternalReferenceStorageType.elasticSearchDoc), -}); - -const ExternalReferenceStorageSOSchema = z.object({ - type: z.literal(ExternalReferenceStorageType.savedObject), - soType: z.string(), -}); - -const ExternalReferenceBaseAttachmentPayloadSchema = z.object({ - externalReferenceAttachmentTypeId: z.string(), - externalReferenceMetadata: z.record(z.string(), jsonValueSchema).nullable(), - type: z.literal(AttachmentType.externalReference), - owner: z.string(), -}); - -export const ExternalReferenceNoSOAttachmentPayloadSchema = - ExternalReferenceBaseAttachmentPayloadSchema.extend({ - externalReferenceId: z.string(), - externalReferenceStorage: ExternalReferenceStorageNoSOSchema, - }); - -export const ExternalReferenceSOAttachmentPayloadSchema = - ExternalReferenceBaseAttachmentPayloadSchema.extend({ - externalReferenceId: z.string(), - externalReferenceStorage: ExternalReferenceStorageSOSchema, - }); - -export const ExternalReferenceSOWithoutRefsAttachmentPayloadSchema = - ExternalReferenceBaseAttachmentPayloadSchema.extend({ - externalReferenceStorage: ExternalReferenceStorageSOSchema, - }); - -export const ExternalReferenceAttachmentPayloadSchema = z.union([ - ExternalReferenceNoSOAttachmentPayloadSchema, - ExternalReferenceSOAttachmentPayloadSchema, -]); - -export const ExternalReferenceWithoutRefsAttachmentPayloadSchema = z.union([ - ExternalReferenceNoSOAttachmentPayloadSchema, - ExternalReferenceSOWithoutRefsAttachmentPayloadSchema, -]); - -const ExternalReferenceAttachmentAttributesSchema = ExternalReferenceAttachmentPayloadSchema.and( - AttachmentAttributesBasicSchema -); - -const ExternalReferenceWithoutRefsAttachmentAttributesSchema = - ExternalReferenceWithoutRefsAttachmentPayloadSchema.and(AttachmentAttributesBasicSchema); - -const ExternalReferenceNoSOAttachmentAttributesSchema = - ExternalReferenceNoSOAttachmentPayloadSchema.merge(AttachmentAttributesBasicSchema); - -const ExternalReferenceSOAttachmentAttributesSchema = - ExternalReferenceSOAttachmentPayloadSchema.merge(AttachmentAttributesBasicSchema); - -export const ExternalReferenceAttachmentSchema = ExternalReferenceAttachmentAttributesSchema.and( - z.object({ id: z.string(), version: z.string() }) -); - -export type ExternalReferenceAttachmentPayload = z.infer< - typeof ExternalReferenceAttachmentPayloadSchema ->; -export type ExternalReferenceSOAttachmentPayload = z.infer< - typeof ExternalReferenceSOAttachmentPayloadSchema ->; -export type ExternalReferenceNoSOAttachmentPayload = z.infer< - typeof ExternalReferenceNoSOAttachmentPayloadSchema ->; -export type ExternalReferenceAttachmentAttributes = z.infer< - typeof ExternalReferenceAttachmentAttributesSchema ->; -export type ExternalReferenceSOAttachmentAttributes = z.infer< - typeof ExternalReferenceSOAttachmentAttributesSchema ->; -export type ExternalReferenceNoSOAttachmentAttributes = z.infer< - typeof ExternalReferenceNoSOAttachmentAttributesSchema ->; -export type ExternalReferenceWithoutRefsAttachmentPayload = z.infer< - typeof ExternalReferenceWithoutRefsAttachmentPayloadSchema ->; -export type ExternalReferenceAttachment = z.infer; - -/** - * Persistable state - */ -export const PersistableStateAttachmentPayloadSchema = z.object({ - type: z.literal(AttachmentType.persistableState), - owner: z.string(), - persistableStateAttachmentTypeId: z.string(), - persistableStateAttachmentState: z.record(z.string(), jsonValueSchema), -}); - -const PersistableStateAttachmentAttributesSchema = PersistableStateAttachmentPayloadSchema.merge( - AttachmentAttributesBasicSchema -); - -export const PersistableStateAttachmentSchema = PersistableStateAttachmentAttributesSchema.extend({ - id: z.string(), - version: z.string(), -}); - -export type PersistableStateAttachmentPayload = z.infer< - typeof PersistableStateAttachmentPayloadSchema ->; -export type PersistableStateAttachment = z.infer; -export type PersistableStateAttachmentAttributes = z.infer< - typeof PersistableStateAttachmentAttributesSchema ->; - -/** - * Common - */ -export const AttachmentPayloadSchema = z.union([ - UserCommentAttachmentPayloadSchema, - AlertAttachmentPayloadSchema, - EventAttachmentPayloadSchema, - ActionsAttachmentPayloadSchema, - ExternalReferenceNoSOAttachmentPayloadSchema, - ExternalReferenceSOAttachmentPayloadSchema, - PersistableStateAttachmentPayloadSchema, -]); - -export const AttachmentAttributesSchema = z.union([ - UserCommentAttachmentAttributesSchema, - AlertAttachmentAttributesSchema, - EventAttachmentAttributesSchema, - ActionsAttachmentAttributesSchema, - ExternalReferenceAttachmentAttributesSchema, - PersistableStateAttachmentAttributesSchema, -]); - -const AttachmentAttributesNoSOSchema = z.union([ - UserCommentAttachmentAttributesSchema, - AlertAttachmentAttributesSchema, - EventAttachmentAttributesSchema, - ActionsAttachmentAttributesSchema, - ExternalReferenceNoSOAttachmentAttributesSchema, - PersistableStateAttachmentAttributesSchema, -]); - -const AttachmentAttributesWithoutRefsSchema = z.union([ - UserCommentAttachmentAttributesSchema, - AlertAttachmentAttributesSchema, - EventAttachmentAttributesSchema, - ActionsAttachmentAttributesSchema, - ExternalReferenceWithoutRefsAttachmentAttributesSchema, - PersistableStateAttachmentAttributesSchema, -]); - -export const AttachmentSchema = AttachmentAttributesSchema.and( - z.object({ id: z.string(), version: z.string() }) -); - -export const AttachmentsSchema = z.array(AttachmentSchema); - -export const AttachmentPatchAttributesSchema = z - .union([ - UserCommentAttachmentPayloadSchema.partial(), - AlertAttachmentPayloadSchema.partial(), - EventAttachmentPayloadSchema.partial(), - ActionsAttachmentPayloadSchema.partial(), - ExternalReferenceNoSOAttachmentPayloadSchema.partial(), - ExternalReferenceSOAttachmentPayloadSchema.partial(), - PersistableStateAttachmentPayloadSchema.partial(), - ]) - .and(AttachmentAttributesBasicSchema.partial()); - -export type AttachmentAttributes = z.infer; -export type AttachmentAttributesNoSO = z.infer; -export type AttachmentAttributesWithoutRefs = z.infer; -export type AttachmentPatchAttributes = z.infer; -export type Attachment = z.infer; -export type Attachments = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/v2.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/v2.ts deleted file mode 100644 index aaebadb4656da..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/attachment/v2.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { jsonValueSchema } from '../../../schema_zod'; -import { - AttachmentAttributesBasicSchema, - AttachmentAttributesSchema, - AttachmentSchema, -} from './v1'; - -export const UnifiedReferenceAttachmentPayloadSchema = z.object({ - type: z.string(), - attachmentId: z.string(), - owner: z.string(), - data: z.record(z.string(), jsonValueSchema).nullable().optional(), - metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), -}); - -export const UnifiedValueAttachmentPayloadSchema = z.object({ - type: z.string(), - data: z.record(z.string(), jsonValueSchema), - owner: z.string(), - metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), -}); - -export const UnifiedAttachmentPayloadSchema = z.union([ - UnifiedReferenceAttachmentPayloadSchema, - UnifiedValueAttachmentPayloadSchema, -]); - -export const UnifiedAttachmentAttributesSchema = UnifiedAttachmentPayloadSchema.and( - AttachmentAttributesBasicSchema -); - -export const UnifiedAttachmentSchema = UnifiedAttachmentAttributesSchema.and( - z.object({ id: z.string(), version: z.string() }) -); - -const UnifiedReferenceAttachmentPayloadPartialSchema = z.object({ - type: z.string().optional(), - attachmentId: z.string().optional(), - data: z.record(z.string(), jsonValueSchema).nullable().optional(), - metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), -}); - -const UnifiedValueAttachmentPayloadPartialSchema = z.object({ - type: z.string().optional(), - data: z.record(z.string(), jsonValueSchema).optional(), - metadata: z.record(z.string(), jsonValueSchema).nullable().optional(), -}); - -export const UnifiedAttachmentPatchAttributesSchema = z - .union([ - UnifiedReferenceAttachmentPayloadPartialSchema, - UnifiedValueAttachmentPayloadPartialSchema, - ]) - .and(AttachmentAttributesBasicSchema.partial()); - -export type UnifiedReferenceAttachmentPayload = z.infer< - typeof UnifiedReferenceAttachmentPayloadSchema ->; -export type UnifiedValueAttachmentPayload = z.infer; -export type UnifiedAttachmentPayload = z.infer; -export type UnifiedAttachmentAttributes = z.infer; -export type UnifiedAttachment = z.infer; - -/** - * Combined v1 legacy and v2 unified attachment types - */ -export const AttachmentSchemaV2 = z.union([AttachmentSchema, UnifiedAttachmentSchema]); -export const AttachmentAttributesSchemaV2 = z.union([ - AttachmentAttributesSchema, - UnifiedAttachmentAttributesSchema, -]); - -export type AttachmentV2 = z.infer; -export type AttachmentAttributesV2 = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/case/v1.ts deleted file mode 100644 index e4b070f0bb21d..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/case/v1.ts +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseStatuses } from '@kbn/cases-components/src/status/types'; -import { - CaseSeverity as BundledCaseSeveritySchema, - CaseStatus as BundledCaseStatusSchema, - Settings, -} from '../../../bundled-types.gen'; -import { CASE_EXTENDED_FIELDS } from '../../../constants'; -import { ExternalServiceSchema } from '../external_service/v1'; -import { CaseAssigneesSchema, UserSchema } from '../user/v1'; -import { CaseConnectorSchema } from '../connector/v1'; -import { AttachmentSchemaV2 } from '../attachment/v2'; -import { CaseCustomFieldsSchema } from '../custom_field/v1'; -import { CaseObservableSchema } from '../observable/v1'; -import { CaseSeverity } from '../../domain/case/v1'; - -export { CaseStatuses, CaseSeverity }; - -/** - * Status - */ -export const CaseStatusSchema = BundledCaseStatusSchema; - -export const caseStatuses = Object.values(CaseStatuses); - -/** - * Severity - */ -export const CaseSeveritySchema = BundledCaseSeveritySchema; - -/** - * Case - */ -export const CaseSettingsSchema = Settings; - -export const CaseTemplateSchema = z.object({ - id: z.string(), - version: z.number(), -}); - -const CaseBaseFields = { - description: z.string(), - tags: z.array(z.string()), - title: z.string(), - connector: CaseConnectorSchema, - severity: CaseSeveritySchema, - assignees: CaseAssigneesSchema, - category: z.string().nullable(), - customFields: CaseCustomFieldsSchema, - settings: CaseSettingsSchema, - observables: z.array(CaseObservableSchema), -}; - -export const CaseBaseOptionalFieldsSchema = z.object({ - description: z.string().optional(), - tags: z.array(z.string()).optional(), - title: z.string().optional(), - connector: CaseConnectorSchema.optional(), - severity: CaseSeveritySchema.optional(), - assignees: CaseAssigneesSchema.optional(), - category: z.string().nullable().optional(), - customFields: CaseCustomFieldsSchema.optional(), - settings: CaseSettingsSchema.optional(), - observables: z.array(CaseObservableSchema).optional(), -}); - -const CaseBasicSchema = z.object({ - status: CaseStatusSchema, - owner: z.string(), - ...CaseBaseFields, -}); - -export const CaseAttributesSchema = CaseBasicSchema.extend({ - duration: z.number().nullable(), - closed_at: z.string().nullable(), - closed_by: UserSchema.nullable(), - created_at: z.string(), - created_by: UserSchema, - external_service: ExternalServiceSchema.nullable(), - updated_at: z.string().nullable(), - updated_by: UserSchema.nullable(), - total_observables: z.number().nullable(), - incremental_id: z.number().nullable().optional(), - in_progress_at: z.string().nullable().optional(), - time_to_acknowledge: z.number().nullable().optional(), - time_to_investigate: z.number().nullable().optional(), - time_to_resolve: z.number().nullable().optional(), - template: CaseTemplateSchema.nullable().optional(), - [CASE_EXTENDED_FIELDS]: z.record(z.string(), z.string()).optional(), -}); - -export const CaseSchema = CaseAttributesSchema.extend({ - id: z.string(), - totalComment: z.number(), - totalAlerts: z.number(), - totalEvents: z.number().optional(), - version: z.string(), - comments: z.array(AttachmentSchemaV2).optional(), -}); - -export const CasesSchema = z.array(CaseSchema); - -export const AttachmentTotalsSchema = z.object({ - alerts: z.number(), - events: z.number(), - userComments: z.number(), -}); - -export const RelatedCaseSchema = z.object({ - id: z.string(), - title: z.string(), - description: z.string(), - status: CaseStatusSchema, - createdAt: z.string(), - totals: AttachmentTotalsSchema, -}); - -export const SimilaritySchema = z.object({ - typeKey: z.string(), - typeLabel: z.string(), - value: z.string(), -}); - -export const SimilarCaseSchema = CaseSchema.extend({ - similarities: z.object({ observables: z.array(SimilaritySchema) }), -}); - -export type Case = z.infer; -export type Cases = z.infer; -export type CaseAttributes = z.infer; -export type CaseSettings = z.infer; -export type RelatedCase = z.infer; -export type AttachmentTotals = z.infer; -export type CaseBaseOptionalFields = z.infer; -export type SimilarCase = z.infer; -export type SimilarCases = SimilarCase[]; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/configure/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/configure/v1.ts deleted file mode 100644 index 713890089ffda..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/configure/v1.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseConnectorSchema, ConnectorMappingsSchema } from '../connector/v1'; -import { UserSchema } from '../user/v1'; -import { - CustomFieldTextTypeSchema, - CustomFieldToggleTypeSchema, - CustomFieldNumberTypeSchema, -} from '../custom_field/v1'; -import { CaseBaseOptionalFieldsSchema } from '../case/v1'; -import { CaseObservableTypeSchema } from '../observable/v1'; - -export const ClosureTypeSchema = z.union([ - z.literal('close-by-user'), - z.literal('close-by-pushing'), -]); - -export const CustomFieldConfigurationWithoutTypeSchema = z.object({ - key: z.string(), - label: z.string(), - required: z.boolean(), -}); - -export const TextCustomFieldConfigurationSchema = CustomFieldConfigurationWithoutTypeSchema.extend({ - type: CustomFieldTextTypeSchema, - defaultValue: z.string().nullable().optional(), -}); - -export const ToggleCustomFieldConfigurationSchema = - CustomFieldConfigurationWithoutTypeSchema.extend({ - type: CustomFieldToggleTypeSchema, - defaultValue: z.boolean().nullable().optional(), - }); - -export const NumberCustomFieldConfigurationSchema = - CustomFieldConfigurationWithoutTypeSchema.extend({ - type: CustomFieldNumberTypeSchema, - defaultValue: z.number().nullable().optional(), - }); - -export const CustomFieldConfigurationSchema = z.union([ - TextCustomFieldConfigurationSchema, - ToggleCustomFieldConfigurationSchema, - NumberCustomFieldConfigurationSchema, -]); - -export const CustomFieldsConfigurationSchema = z.array(CustomFieldConfigurationSchema); - -export const ObservableTypesConfigurationSchema = z.array(CaseObservableTypeSchema); - -export const TemplateConfigurationSchema = z.object({ - key: z.string(), - name: z.string(), - caseFields: CaseBaseOptionalFieldsSchema.nullable(), - description: z.string().optional(), - tags: z.array(z.string()).optional(), -}); - -export const TemplatesConfigurationSchema = z.array(TemplateConfigurationSchema); - -export const ConfigurationBasicWithoutOwnerSchema = z.object({ - connector: CaseConnectorSchema, - closure_type: ClosureTypeSchema, - customFields: CustomFieldsConfigurationSchema, - templates: TemplatesConfigurationSchema, - observableTypes: ObservableTypesConfigurationSchema, -}); - -export const CasesConfigureBasicSchema = ConfigurationBasicWithoutOwnerSchema.extend({ - owner: z.string(), -}); - -export const ConfigurationActivityFieldsSchema = z.object({ - created_at: z.string(), - created_by: UserSchema, - updated_at: z.string().nullable(), - updated_by: UserSchema.nullable(), -}); - -export const ConfigurationAttributesSchema = CasesConfigureBasicSchema.merge( - ConfigurationActivityFieldsSchema -); - -export const ConfigurationSchema = ConfigurationAttributesSchema.extend({ - id: z.string(), - version: z.string(), - error: z.string().nullable(), - owner: z.string(), - mappings: ConnectorMappingsSchema, -}); - -export const ConfigurationsSchema = z.array(ConfigurationSchema); - -export type CustomFieldsConfiguration = z.infer; -export type CustomFieldConfiguration = z.infer; -export type TemplatesConfiguration = z.infer; -export type TemplateConfiguration = z.infer; -export type ClosureType = z.infer; -export type ConfigurationAttributes = z.infer; -export type Configuration = z.infer; -export type Configurations = z.infer; -export type ObservableTypesConfiguration = z.infer; -export type ObservableTypeConfiguration = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/connector/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/connector/v1.ts deleted file mode 100644 index 408dc13eb457a..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/connector/v1.ts +++ /dev/null @@ -1,197 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { ConnectorTypes, SwimlaneConnectorType } from '../../domain/connector/v1'; - -export { ConnectorTypes, SwimlaneConnectorType }; -export type { ActionConnector, ActionTypeConnector } from '../../domain/connector/v1'; - -const ConnectorCasesWebhookTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.casesWebhook), - fields: z.null(), -}); - -/** - * Jira - */ -export const JiraFieldsSchema = z.object({ - issueType: z.string().nullable(), - priority: z.string().nullable(), - parent: z.string().nullable(), - otherFields: z.string().nullable().optional(), -}); - -const ConnectorJiraTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.jira), - fields: JiraFieldsSchema.nullable(), -}); - -/** - * Resilient - */ -export const ResilientFieldsSchema = z.object({ - incidentTypes: z.array(z.string()).nullable(), - severityCode: z.string().nullable(), - additionalFields: z.string().nullable().optional(), -}); - -const ConnectorResilientTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.resilient), - fields: ResilientFieldsSchema.nullable(), -}); - -/** - * ServiceNow ITSM - */ -export const ServiceNowITSMFieldsSchema = z.object({ - impact: z.string().nullable(), - severity: z.string().nullable(), - urgency: z.string().nullable(), - category: z.string().nullable(), - subcategory: z.string().nullable(), - additionalFields: z.string().nullable().optional(), -}); - -const ConnectorServiceNowITSMTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.serviceNowITSM), - fields: ServiceNowITSMFieldsSchema.nullable(), -}); - -/** - * ServiceNow SIR - */ -export const ServiceNowSIRFieldsSchema = z.object({ - category: z.string().nullable(), - destIp: z.boolean().nullable(), - malwareHash: z.boolean().nullable(), - malwareUrl: z.boolean().nullable(), - priority: z.string().nullable(), - sourceIp: z.boolean().nullable(), - subcategory: z.string().nullable(), - additionalFields: z.string().nullable().optional(), -}); - -const ConnectorServiceNowSIRTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.serviceNowSIR), - fields: ServiceNowSIRFieldsSchema.nullable(), -}); - -/** - * Swimlane - */ -export const SwimlaneFieldsSchema = z.object({ - caseId: z.string().nullable(), -}); - -const ConnectorSwimlaneTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.swimlane), - fields: SwimlaneFieldsSchema.nullable(), -}); - -/** - * TheHive - */ -export const TheHiveFieldsSchema = z.object({ - tlp: z.number().nullable(), -}); - -const ConnectorTheHiveTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.theHive), - fields: TheHiveFieldsSchema.nullable(), -}); - -/** - * None connector - */ -const ConnectorNoneTypeFieldsSchema = z.object({ - type: z.literal(ConnectorTypes.none), - fields: z.null(), -}); - -export const ConnectorTypeFieldsSchema = z.discriminatedUnion('type', [ - ConnectorCasesWebhookTypeFieldsSchema, - ConnectorJiraTypeFieldsSchema, - ConnectorNoneTypeFieldsSchema, - ConnectorResilientTypeFieldsSchema, - ConnectorServiceNowITSMTypeFieldsSchema, - ConnectorServiceNowSIRTypeFieldsSchema, - ConnectorSwimlaneTypeFieldsSchema, - ConnectorTheHiveTypeFieldsSchema, -]); - -const NameSchema = z.object({ name: z.string() }); - -export const CaseUserActionConnectorSchema = z.discriminatedUnion('type', [ - ConnectorCasesWebhookTypeFieldsSchema.merge(NameSchema), - ConnectorJiraTypeFieldsSchema.merge(NameSchema), - ConnectorNoneTypeFieldsSchema.merge(NameSchema), - ConnectorResilientTypeFieldsSchema.merge(NameSchema), - ConnectorServiceNowITSMTypeFieldsSchema.merge(NameSchema), - ConnectorServiceNowSIRTypeFieldsSchema.merge(NameSchema), - ConnectorSwimlaneTypeFieldsSchema.merge(NameSchema), - ConnectorTheHiveTypeFieldsSchema.merge(NameSchema), -]); - -const IdSchema = z.object({ id: z.string() }); - -export const CaseConnectorSchema = z.discriminatedUnion('type', [ - ConnectorCasesWebhookTypeFieldsSchema.merge(NameSchema).merge(IdSchema), - ConnectorJiraTypeFieldsSchema.merge(NameSchema).merge(IdSchema), - ConnectorNoneTypeFieldsSchema.merge(NameSchema).merge(IdSchema), - ConnectorResilientTypeFieldsSchema.merge(NameSchema).merge(IdSchema), - ConnectorServiceNowITSMTypeFieldsSchema.merge(NameSchema).merge(IdSchema), - ConnectorServiceNowSIRTypeFieldsSchema.merge(NameSchema).merge(IdSchema), - ConnectorSwimlaneTypeFieldsSchema.merge(NameSchema).merge(IdSchema), - ConnectorTheHiveTypeFieldsSchema.merge(NameSchema).merge(IdSchema), -]); - -/** - * Mappings - */ -const ConnectorMappingActionTypeSchema = z.union([ - z.literal('append'), - z.literal('nothing'), - z.literal('overwrite'), -]); - -const ConnectorMappingSourceSchema = z.union([ - z.literal('title'), - z.literal('description'), - z.literal('comments'), - z.literal('tags'), -]); - -const ConnectorMappingTargetSchema = z.union([z.string(), z.literal('not_mapped')]); - -const ConnectorMappingSchema = z.object({ - action_type: ConnectorMappingActionTypeSchema, - source: ConnectorMappingSourceSchema, - target: ConnectorMappingTargetSchema, -}); - -export const ConnectorMappingsSchema = z.array(ConnectorMappingSchema); - -export const ConnectorMappingsAttributesSchema = z.object({ - mappings: ConnectorMappingsSchema, - owner: z.string(), -}); - -export type ConnectorMappingsAttributes = z.infer; -export type ConnectorMappings = z.infer; -export type ConnectorMappingActionType = z.infer; -export type ConnectorMappingSource = z.infer; -export type ConnectorMappingTarget = z.infer; -export type CaseUserActionConnector = z.infer; -export type CaseConnector = z.infer; -export type ConnectorTypeFields = z.infer; -export type JiraFieldsType = z.infer; -export type ResilientFieldsType = z.infer; -export type SwimlaneFieldsType = z.infer; -export type ServiceNowITSMFieldsType = z.infer; -export type ServiceNowSIRFieldsType = z.infer; -export type TheHiveFieldsType = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/custom_field/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/custom_field/v1.ts deleted file mode 100644 index e11bb7c4d5bd8..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/custom_field/v1.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CustomFieldTypes } from '../../domain/custom_field/v1'; - -export { CustomFieldTypes }; - -export const CustomFieldTextTypeSchema = z.literal(CustomFieldTypes.TEXT); -export const CustomFieldToggleTypeSchema = z.literal(CustomFieldTypes.TOGGLE); -export const CustomFieldNumberTypeSchema = z.literal(CustomFieldTypes.NUMBER); - -const CaseCustomFieldTextSchema = z.object({ - key: z.string(), - type: CustomFieldTextTypeSchema, - value: z.string().nullable(), -}); - -export const CaseCustomFieldToggleSchema = z.object({ - key: z.string(), - type: CustomFieldToggleTypeSchema, - value: z.boolean().nullable(), -}); - -export const CaseCustomFieldNumberSchema = z.object({ - key: z.string(), - type: CustomFieldNumberTypeSchema, - value: z.number().nullable(), -}); - -export const CaseCustomFieldSchema = z.union([ - CaseCustomFieldTextSchema, - CaseCustomFieldToggleSchema, - CaseCustomFieldNumberSchema, -]); - -export const CaseCustomFieldsSchema = z.array(CaseCustomFieldSchema); - -export type CaseCustomFields = z.infer; -export type CaseCustomField = z.infer; -export type CaseCustomFieldToggle = z.infer; -export type CaseCustomFieldText = z.infer; -export type CaseCustomFieldNumber = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/external_service/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/external_service/v1.ts deleted file mode 100644 index 7d46489987c97..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/external_service/v1.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserSchema } from '../user/v1'; - -export const ExternalServiceBasicSchema = z.object({ - connector_name: z.string(), - external_id: z.string(), - external_title: z.string(), - external_url: z.string(), - pushed_at: z.string(), - pushed_by: UserSchema, -}); - -export const ExternalServiceSchema = ExternalServiceBasicSchema.extend({ - connector_id: z.string(), -}); - -export type ExternalService = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/observable/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/observable/v1.ts deleted file mode 100644 index 2d81dc970ab1e..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/observable/v1.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; - -export const CaseObservableBaseSchema = z.object({ - typeKey: z.string(), - value: z.string(), - description: z.string().nullable(), -}); - -export const CaseObservableSchema = CaseObservableBaseSchema.extend({ - id: z.string(), - createdAt: z.string(), - updatedAt: z.string().nullable(), -}); - -export const CaseObservableTypeSchema = z.object({ - key: z.string(), - label: z.string(), -}); - -export type Observable = z.infer; -export type ObservableType = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user/v1.ts deleted file mode 100644 index ff983956253c1..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user/v1.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; - -const UserWithoutProfileUidSchema = z.object({ - email: z.string().nullable().optional(), - full_name: z.string().nullable().optional(), - username: z.string().nullable().optional(), -}); - -export const UserSchema = UserWithoutProfileUidSchema.extend({ - profile_uid: z.string().optional(), -}); - -export const UserWithProfileInfoSchema = z.object({ - user: UserWithoutProfileUidSchema, - uid: z.string().optional(), - avatar: z - .object({ - initials: z.string().nullable(), - color: z.string().nullable(), - imageUrl: z.string().nullable(), - }) - .optional(), -}); - -export const UsersSchema = z.array(UserSchema); - -export type User = z.infer; -export type UserWithProfileInfo = z.infer; - -export const CaseUserProfileSchema = z.object({ - uid: z.string(), -}); - -export type CaseUserProfile = z.infer; - -/** - * Assignees - */ -export const CaseAssigneesSchema = z.array(CaseUserProfileSchema); -export type CaseAssignees = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/action/v1.ts deleted file mode 100644 index a2a16516b483e..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/action/v1.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes, UserActionActions } from '../../../domain/user_action/action/v1'; - -export { UserActionTypes, UserActionActions }; -export type { UserActionType, UserActionAction } from '../../../domain/user_action/action/v1'; - -export const UserActionActionsSchema = z.enum( - Object.values(UserActionActions) as [string, ...string[]] -); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/assignees/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/assignees/v1.ts deleted file mode 100644 index 27297c24331c3..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/assignees/v1.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseAssigneesSchema } from '../../user/v1'; -import { UserActionTypes } from '../action/v1'; - -export const AssigneesUserActionPayloadSchema = z.object({ assignees: CaseAssigneesSchema }); - -export const AssigneesUserActionSchema = z.object({ - type: z.literal(UserActionTypes.assignees), - payload: AssigneesUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/category/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/category/v1.ts deleted file mode 100644 index d485e94e386d0..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/category/v1.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; - -export const CategoryUserActionPayloadSchema = z.object({ - category: z.string().nullable(), -}); - -export const CategoryUserActionSchema = z.object({ - type: z.literal(UserActionTypes.category), - payload: CategoryUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/comment/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/comment/v1.ts deleted file mode 100644 index 8ffb1bad4a6bf..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/comment/v1.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; -import { - AttachmentRequestSchemaV2, - AttachmentRequestWithoutRefsSchemaV2, -} from '../../../api_zod/attachment/v2'; - -export const CommentUserActionPayloadSchema = z.object({ comment: AttachmentRequestSchemaV2 }); - -export const CommentUserActionPayloadWithoutIdsSchema = z.object({ - comment: AttachmentRequestWithoutRefsSchemaV2, -}); - -export const CommentUserActionSchema = z.object({ - type: z.literal(UserActionTypes.comment), - payload: CommentUserActionPayloadSchema, -}); - -export const CommentUserActionWithoutIdsSchema = CommentUserActionSchema; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/connector/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/connector/v1.ts deleted file mode 100644 index 581e3a1160e29..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/connector/v1.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseUserActionConnectorSchema, CaseConnectorSchema } from '../../connector/v1'; -import { UserActionTypes } from '../action/v1'; - -export const ConnectorUserActionPayloadWithoutConnectorIdSchema = z.object({ - connector: CaseUserActionConnectorSchema, -}); - -export const ConnectorUserActionPayloadSchema = z.object({ - connector: CaseConnectorSchema, -}); - -export const ConnectorUserActionWithoutConnectorIdSchema = z.object({ - type: z.literal(UserActionTypes.connector), - payload: ConnectorUserActionPayloadWithoutConnectorIdSchema, -}); - -export const ConnectorUserActionSchema = z.object({ - type: z.literal(UserActionTypes.connector), - payload: ConnectorUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/create_case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/create_case/v1.ts deleted file mode 100644 index d4b9604df8843..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/create_case/v1.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; -import { AssigneesUserActionPayloadSchema } from '../assignees/v1'; -import { CategoryUserActionPayloadSchema } from '../category/v1'; -import { - ConnectorUserActionPayloadSchema, - ConnectorUserActionPayloadWithoutConnectorIdSchema, -} from '../connector/v1'; -import { CustomFieldsUserActionPayloadSchema } from '../custom_fields/v1'; -import { DescriptionUserActionPayloadSchema } from '../description/v1'; -import { SettingsUserActionPayloadSchema } from '../settings/v1'; -import { TagsUserActionPayloadSchema } from '../tags/v1'; -import { TitleUserActionPayloadSchema } from '../title/v1'; - -const CommonPayloadAttributesSchema = z.object({ - assignees: AssigneesUserActionPayloadSchema.shape.assignees, - description: DescriptionUserActionPayloadSchema.shape.description, - status: z.string(), - severity: z.string(), - tags: TagsUserActionPayloadSchema.shape.tags, - title: TitleUserActionPayloadSchema.shape.title, - settings: SettingsUserActionPayloadSchema.shape.settings, - owner: z.string(), - category: CategoryUserActionPayloadSchema.shape.category.optional(), - customFields: CustomFieldsUserActionPayloadSchema.shape.customFields.optional(), -}); - -export const CreateCaseUserActionSchema = z.object({ - type: z.literal(UserActionTypes.create_case), - payload: ConnectorUserActionPayloadSchema.merge(CommonPayloadAttributesSchema), -}); - -export const CreateCaseUserActionWithoutConnectorIdSchema = z.object({ - type: z.literal(UserActionTypes.create_case), - payload: ConnectorUserActionPayloadWithoutConnectorIdSchema.merge(CommonPayloadAttributesSchema), -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/custom_fields/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/custom_fields/v1.ts deleted file mode 100644 index 98aa017766455..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/custom_fields/v1.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseCustomFieldsSchema } from '../../custom_field/v1'; -import { UserActionTypes } from '../action/v1'; - -export const CustomFieldsUserActionPayloadSchema = z.object({ - customFields: CaseCustomFieldsSchema, -}); - -export const CustomFieldsUserActionSchema = z.object({ - type: z.literal(UserActionTypes.customFields), - payload: CustomFieldsUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/delete_case/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/delete_case/v1.ts deleted file mode 100644 index b7b4dc6a24874..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/delete_case/v1.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; - -export const DeleteCaseUserActionSchema = z.object({ - type: z.literal(UserActionTypes.delete_case), - payload: z.object({}), -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/description/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/description/v1.ts deleted file mode 100644 index e5faab0f34bf6..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/description/v1.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; - -export const DescriptionUserActionPayloadSchema = z.object({ description: z.string() }); - -export const DescriptionUserActionSchema = z.object({ - type: z.literal(UserActionTypes.description), - payload: DescriptionUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/observables/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/observables/v1.ts deleted file mode 100644 index 51f7e96abffa5..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/observables/v1.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; - -const ObservablesActionTypeSchema = z.union([ - z.literal('add'), - z.literal('delete'), - z.literal('update'), -]); - -export const ObservablePayloadSchema = z.object({ - count: z.number(), - actionType: ObservablesActionTypeSchema, -}); - -export const ObservablesUserActionPayloadSchema = z.object({ - observables: ObservablePayloadSchema, -}); - -export const ObservablesUserActionSchema = z.object({ - type: z.literal(UserActionTypes.observables), - payload: ObservablesUserActionPayloadSchema, -}); - -export type ObservablesActionType = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/pushed/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/pushed/v1.ts deleted file mode 100644 index 1f4aea3fd7ade..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/pushed/v1.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { ExternalServiceBasicSchema, ExternalServiceSchema } from '../../external_service/v1'; -import { UserActionTypes } from '../action/v1'; - -export const PushedUserActionPayloadWithoutConnectorIdSchema = z.object({ - externalService: ExternalServiceBasicSchema, -}); - -export const PushedUserActionPayloadSchema = z.object({ - externalService: ExternalServiceSchema, -}); - -export const PushedUserActionWithoutConnectorIdSchema = z.object({ - type: z.literal(UserActionTypes.pushed), - payload: PushedUserActionPayloadWithoutConnectorIdSchema, -}); - -export const PushedUserActionSchema = z.object({ - type: z.literal(UserActionTypes.pushed), - payload: PushedUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/settings/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/settings/v1.ts deleted file mode 100644 index da0ade181becc..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/settings/v1.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; - -export const SettingsUserActionPayloadSchema = z.object({ - settings: z.object({ - syncAlerts: z.boolean().optional(), - extractObservables: z.boolean().optional(), - }), -}); - -export const SettingsUserActionSchema = z.object({ - type: z.literal(UserActionTypes.settings), - payload: SettingsUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/severity/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/severity/v1.ts deleted file mode 100644 index f93d009e3cd57..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/severity/v1.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseSeveritySchema } from '../../case/v1'; -import { UserActionTypes } from '../action/v1'; - -export const SeverityUserActionPayloadSchema = z.object({ severity: CaseSeveritySchema }); - -export const SeverityUserActionSchema = z.object({ - type: z.literal(UserActionTypes.severity), - payload: SeverityUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/status/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/status/v1.ts deleted file mode 100644 index 56d86fde937f6..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/status/v1.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { CaseStatusSchema } from '../../case/v1'; -import { UserActionTypes } from '../action/v1'; - -export const StatusUserActionPayloadSchema = z.object({ status: CaseStatusSchema }); - -export const StatusUserActionSchema = z.object({ - type: z.literal(UserActionTypes.status), - payload: StatusUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/tags/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/tags/v1.ts deleted file mode 100644 index f23666bfb27c7..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/tags/v1.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; - -export const TagsUserActionPayloadSchema = z.object({ tags: z.array(z.string()) }); - -export const TagsUserActionSchema = z.object({ - type: z.literal(UserActionTypes.tags), - payload: TagsUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/title/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/title/v1.ts deleted file mode 100644 index d893b61a045c0..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/title/v1.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserActionTypes } from '../action/v1'; - -export const TitleUserActionPayloadSchema = z.object({ title: z.string() }); - -export const TitleUserActionSchema = z.object({ - type: z.literal(UserActionTypes.title), - payload: TitleUserActionPayloadSchema, -}); diff --git a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/v1.ts b/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/v1.ts deleted file mode 100644 index b9c4b90d62d5b..0000000000000 --- a/x-pack/platform/plugins/shared/cases/common/types/domain_zod/user_action/v1.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { z } from '@kbn/zod/v4'; -import { UserSchema } from '../user/v1'; -import { UserActionActionsSchema } from './action/v1'; -import { AssigneesUserActionSchema } from './assignees/v1'; -import { CategoryUserActionSchema } from './category/v1'; -import { CommentUserActionSchema, CommentUserActionWithoutIdsSchema } from './comment/v1'; -import { - ConnectorUserActionSchema, - ConnectorUserActionWithoutConnectorIdSchema, -} from './connector/v1'; -import { - CreateCaseUserActionSchema, - CreateCaseUserActionWithoutConnectorIdSchema, -} from './create_case/v1'; -import { DeleteCaseUserActionSchema } from './delete_case/v1'; -import { DescriptionUserActionSchema } from './description/v1'; -import { PushedUserActionSchema, PushedUserActionWithoutConnectorIdSchema } from './pushed/v1'; -import { SettingsUserActionSchema } from './settings/v1'; -import { SeverityUserActionSchema } from './severity/v1'; -import { StatusUserActionSchema } from './status/v1'; -import { TagsUserActionSchema } from './tags/v1'; -import { TitleUserActionSchema } from './title/v1'; -import { CustomFieldsUserActionSchema } from './custom_fields/v1'; -import { ObservablesUserActionSchema } from './observables/v1'; - -export { UserActionTypes, UserActionActions } from './action/v1'; -export { StatusUserActionSchema } from './status/v1'; -export type { UserActionType, UserActionAction } from './action/v1'; - -const UserActionCommonAttributesSchema = z.object({ - created_at: z.string(), - created_by: UserSchema, - owner: z.string(), - action: UserActionActionsSchema, -}); - -export const CaseUserActionInjectedDeprecatedIdsSchema = z.object({ - action_id: z.string(), - case_id: z.string(), - comment_id: z.string().nullable(), -}); - -export const CaseUserActionInjectedIdsSchema = z.object({ - comment_id: z.string().nullable(), -}); - -const BasicUserActionsSchema = z.union([ - DescriptionUserActionSchema, - TagsUserActionSchema, - TitleUserActionSchema, - SettingsUserActionSchema, - StatusUserActionSchema, - SeverityUserActionSchema, - AssigneesUserActionSchema, - DeleteCaseUserActionSchema, - CategoryUserActionSchema, - CustomFieldsUserActionSchema, - ObservablesUserActionSchema, -]); - -const CommonUserActionsWithIdsSchema = z.union([BasicUserActionsSchema, CommentUserActionSchema]); -const CommonUserActionsWithoutIdsSchema = z.union([ - BasicUserActionsSchema, - CommentUserActionWithoutIdsSchema, -]); - -const UserActionPayloadSchema = z.union([ - CommonUserActionsWithIdsSchema, - CreateCaseUserActionSchema, - ConnectorUserActionSchema, - PushedUserActionSchema, -]); - -const UserActionsWithoutIdsSchema = z.union([ - CommonUserActionsWithoutIdsSchema, - CreateCaseUserActionWithoutConnectorIdSchema, - ConnectorUserActionWithoutConnectorIdSchema, - PushedUserActionWithoutConnectorIdSchema, -]); - -export const CaseUserActionBasicSchema = UserActionPayloadSchema.and( - UserActionCommonAttributesSchema -); - -export const CaseUserActionWithoutReferenceIdsSchema = UserActionsWithoutIdsSchema.and( - UserActionCommonAttributesSchema -); - -export const UserActionAttributesSchema = CaseUserActionBasicSchema.and( - CaseUserActionInjectedIdsSchema -); - -const UserActionSchema = UserActionAttributesSchema.and( - z.object({ id: z.string(), version: z.string() }) -); - -export const UserActionsSchema = z.array(UserActionSchema); - -export type CaseUserActionWithoutReferenceIds = z.infer< - typeof CaseUserActionWithoutReferenceIdsSchema ->; -export type UserActionPayload = z.infer; -export type UserActionAttributes = z.infer; -export type UserActions = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/moon.yml b/x-pack/platform/plugins/shared/cases/moon.yml index 4631a138a0169..d1c1c26306fe9 100644 --- a/x-pack/platform/plugins/shared/cases/moon.yml +++ b/x-pack/platform/plugins/shared/cases/moon.yml @@ -30,7 +30,6 @@ dependsOn: - '@kbn/kibana-utils-plugin' - '@kbn/i18n' - '@kbn/utility-types' - - '@kbn/securitysolution-io-ts-utils' - '@kbn/cases-components' - '@kbn/es-query' - '@kbn/i18n-react' @@ -103,6 +102,7 @@ dependsOn: - '@kbn/core-notifications-browser-mocks' - '@kbn/react-query' - '@kbn/zod' + - '@kbn/zod-helpers' - '@kbn/securitysolution-es-utils' - '@kbn/response-ops-retry-service' - '@kbn/openapi-generator' diff --git a/x-pack/platform/plugins/shared/cases/public/api/decoders.ts b/x-pack/platform/plugins/shared/cases/public/api/decoders.ts index 35d9fed6db5aa..6c1a6073b89cf 100644 --- a/x-pack/platform/plugins/shared/cases/public/api/decoders.ts +++ b/x-pack/platform/plugins/shared/cases/public/api/decoders.ts @@ -5,9 +5,7 @@ * 2.0. */ -import { fold } from 'fp-ts/Either'; -import { identity } from 'fp-ts/function'; -import { pipe } from 'fp-ts/pipeable'; +import type { ZodType } from '@kbn/zod/v4'; import type { CasesFindResponse, @@ -16,30 +14,33 @@ import type { CasesSimilarResponse, } from '../../common/types/api'; import { - CasesFindResponseRt, - CasesBulkGetResponseRt, - CasesMetricsResponseRt, - CasesSimilarResponseRt, + CasesFindResponseSchema, + CasesBulkGetResponseSchema, + CasesMetricsResponseSchema, + CasesSimilarResponseSchema, } from '../../common/types/api'; -import { createToasterPlainError } from '../containers/utils'; -import { throwErrors } from '../../common'; +import { ToasterError } from '../containers/utils'; + +const decodeWithToasterError = (schema: ZodType, value: T): NonNullable => { + const result = schema.safeParse(value); + if (result.success) return result.data as NonNullable; + throw new ToasterError([ + result.error.issues + .map((issue) => `${issue.path.join('.') || ''}: ${issue.message}`) + .join(','), + ]); +}; export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => - pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); + decodeWithToasterError(CasesFindResponseSchema, respCases); + export const decodeCasesMetricsResponse = (metrics?: CasesMetricsResponse) => - pipe( - CasesMetricsResponseRt.decode(metrics), - fold(throwErrors(createToasterPlainError), identity) - ); + decodeWithToasterError(CasesMetricsResponseSchema, metrics); export const decodeCasesBulkGetResponse = (res: CasesBulkGetResponse) => { - pipe(CasesBulkGetResponseRt.decode(res), fold(throwErrors(createToasterPlainError), identity)); - + decodeWithToasterError(CasesBulkGetResponseSchema, res); return res; }; export const decodeCasesSimilarResponse = (respCases?: CasesSimilarResponse) => - pipe( - CasesSimilarResponseRt.decode(respCases), - fold(throwErrors(createToasterPlainError), identity) - ); + decodeWithToasterError(CasesSimilarResponseSchema, respCases); diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.test.ts b/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.test.ts index 3b729b139f6e7..18abcc49eaa25 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.test.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.test.ts @@ -5,10 +5,9 @@ * 2.0. */ -import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; import { omit, pick } from 'lodash'; import { DEFAULT_CASES_TABLE_STATE } from '../../containers/constants'; -import { AllCasesURLQueryParamsRt, validateSchema } from './schema'; +import { AllCasesURLQueryParamsSchema, validateSchema } from './schema'; describe('Schema', () => { const supportedFilterOptions = pick(DEFAULT_CASES_TABLE_STATE.filterOptions, [ @@ -27,15 +26,16 @@ describe('Schema', () => { ...DEFAULT_CASES_TABLE_STATE.queryParams, }; - describe('AllCasesURLQueryParamsRt', () => { - it('decodes correctly with defaults', () => { - const [params, errors] = validateNonExact(defaultState, AllCasesURLQueryParamsRt); - - expect(params).toEqual(defaultState); - expect(errors).toEqual(null); + describe('AllCasesURLQueryParamsSchema', () => { + it('parses correctly with defaults', () => { + const result = AllCasesURLQueryParamsSchema.safeParse(defaultState); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(defaultState); + } }); - it('decodes correctly with values', () => { + it('parses correctly with values', () => { const state = { assignees: ['elastic'], tags: ['a', 'b'], @@ -52,48 +52,49 @@ describe('Schema', () => { perPage: 20, }; - const [params, errors] = validateNonExact(state, AllCasesURLQueryParamsRt); - - expect(params).toEqual(state); - expect(errors).toEqual(null); + const result = AllCasesURLQueryParamsSchema.safeParse(state); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(state); + } }); - it('does not throws an error when missing fields', () => { + it('does not throw when missing fields', () => { for (const [key] of Object.entries(defaultState)) { const stateWithoutKey = omit(defaultState, key); - const [params, errors] = validateNonExact(stateWithoutKey, AllCasesURLQueryParamsRt); - - expect(params).toEqual(stateWithoutKey); - expect(errors).toEqual(null); + const result = AllCasesURLQueryParamsSchema.safeParse(stateWithoutKey); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual(stateWithoutKey); + } } }); - it('removes unknown properties', () => { - const [params, errors] = validateNonExact({ page: 10, foo: 'bar' }, AllCasesURLQueryParamsRt); - - expect(params).toEqual({ page: 10 }); - expect(errors).toEqual(null); + it('strips unknown properties', () => { + const result = AllCasesURLQueryParamsSchema.safeParse({ page: 10, foo: 'bar' }); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data).toEqual({ page: 10 }); + } }); it.each(['status', 'severity', 'sortOrder', 'sortField', 'page', 'perPage'])( - 'throws if %s has invalid value', + 'fails if %s has invalid value', (key) => { - const [params, errors] = validateNonExact({ [key]: 'foo' }, AllCasesURLQueryParamsRt); - - expect(params).toEqual(null); - expect(errors).toEqual(`Invalid value "foo" supplied to "${key}"`); + const result = AllCasesURLQueryParamsSchema.safeParse({ [key]: 'foo' }); + expect(result.success).toBe(false); } ); }); describe('validateSchema', () => { it('validates schema correctly', () => { - const params = validateSchema(defaultState, AllCasesURLQueryParamsRt); + const params = validateSchema(defaultState, AllCasesURLQueryParamsSchema); expect(params).toEqual(defaultState); }); - it('throws an error if the schema is not valid', () => { - const params = validateSchema({ severity: 'foo' }, AllCasesURLQueryParamsRt); + it('returns null if the schema is not valid', () => { + const params = validateSchema({ severity: 'foo' }, AllCasesURLQueryParamsSchema); expect(params).toEqual(null); }); }); diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.ts b/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.ts index a0867eb412f63..67f7bdd586ee7 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/schema.ts @@ -5,44 +5,36 @@ * 2.0. */ -import { isLeft } from 'fp-ts/Either'; -import * as rt from 'io-ts'; -import { CaseSeverityRt, CaseStatusRt } from '../../../common/types/domain'; +import { z } from '@kbn/zod/v4'; +import { CaseSeveritySchema, CaseStatusSchema } from '../../../common/types/domain'; -export const AllCasesURLQueryParamsRt = rt.exact( - rt.partial({ - search: rt.string, - severity: rt.array(CaseSeverityRt), - status: rt.array(CaseStatusRt), - tags: rt.array(rt.string), - category: rt.array(rt.string), - assignees: rt.array(rt.union([rt.string, rt.null])), - customFields: rt.record(rt.string, rt.array(rt.string)), - from: rt.string, - to: rt.string, - sortOrder: rt.union([rt.literal('asc'), rt.literal('desc')]), - sortField: rt.union([ - rt.literal('closedAt'), - rt.literal('createdAt'), - rt.literal('updatedAt'), - rt.literal('severity'), - rt.literal('status'), - rt.literal('title'), - rt.literal('category'), +export const AllCasesURLQueryParamsSchema = z + .object({ + search: z.string(), + severity: z.array(CaseSeveritySchema), + status: z.array(CaseStatusSchema), + tags: z.array(z.string()), + category: z.array(z.string()), + assignees: z.array(z.union([z.string(), z.null()])), + customFields: z.record(z.string(), z.array(z.string())), + from: z.string(), + to: z.string(), + sortOrder: z.union([z.literal('asc'), z.literal('desc')]), + sortField: z.union([ + z.literal('closedAt'), + z.literal('createdAt'), + z.literal('updatedAt'), + z.literal('severity'), + z.literal('status'), + z.literal('title'), + z.literal('category'), ]), - page: rt.number, - perPage: rt.number, + page: z.number(), + perPage: z.number(), }) -); + .partial(); -export const validateSchema = ( - obj: unknown, - schema: T -): rt.TypeOf | null => { - const decoded = schema.decode(obj); - if (isLeft(decoded)) { - return null; - } else { - return decoded.right; - } +export const validateSchema = (obj: unknown, schema: z.ZodType): T | null => { + const result = schema.safeParse(obj); + return result.success ? result.data : null; }; diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/types.ts b/x-pack/platform/plugins/shared/cases/public/components/all_cases/types.ts index 4a1dd61aa505c..9584190173d3d 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/types.ts @@ -5,9 +5,9 @@ * 2.0. */ -import type * as rt from 'io-ts'; +import type { z } from '@kbn/zod/v4'; import type { FilterOptions, QueryParams, SortOrder } from '../../../common/ui'; -import type { AllCasesURLQueryParamsRt } from './schema'; +import type { AllCasesURLQueryParamsSchema } from './schema'; export const CASES_TABLE_PER_PAGE_VALUES = [10, 25, 50, 100]; @@ -51,4 +51,4 @@ export interface AllCasesURLState { queryParams: Partial; } -export type AllCasesURLQueryParams = rt.TypeOf; +export type AllCasesURLQueryParams = z.infer; diff --git a/x-pack/platform/plugins/shared/cases/public/components/all_cases/utils/parse_url_params.tsx b/x-pack/platform/plugins/shared/cases/public/components/all_cases/utils/parse_url_params.tsx index 1ad31f20f94fc..4e23560ad5aa8 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/all_cases/utils/parse_url_params.tsx +++ b/x-pack/platform/plugins/shared/cases/public/components/all_cases/utils/parse_url_params.tsx @@ -13,7 +13,7 @@ import { DEFAULT_CASES_TABLE_STATE } from '../../../containers/constants'; import { stringToIntegerWithDefault } from '.'; import { SortFieldCase } from '../../../../common/ui'; import { LEGACY_SUPPORTED_STATE_KEYS, ALL_CASES_STATE_URL_KEY } from '../constants'; -import { AllCasesURLQueryParamsRt, validateSchema } from '../schema'; +import { AllCasesURLQueryParamsSchema, validateSchema } from '../schema'; import type { AllCasesURLQueryParams } from '../types'; type LegacySupportedKeys = (typeof LEGACY_SUPPORTED_STATE_KEYS)[number]; @@ -109,7 +109,10 @@ export function parseUrlParams(urlParams: URLSearchParams): AllCasesURLQueryPara return {}; } - const validatedAllCasesParams = validateSchema(parsedAllCasesParams, AllCasesURLQueryParamsRt); + const validatedAllCasesParams = validateSchema( + parsedAllCasesParams, + AllCasesURLQueryParamsSchema + ); if (!validatedAllCasesParams) { return {}; @@ -119,7 +122,10 @@ export function parseUrlParams(urlParams: URLSearchParams): AllCasesURLQueryPara } const parseAndValidateLegacyUrl = (urlParams: URLSearchParams): AllCasesURLQueryParams => { - const validatedUrlParams = validateSchema(parseLegacyUrl(urlParams), AllCasesURLQueryParamsRt); + const validatedUrlParams = validateSchema( + parseLegacyUrl(urlParams), + AllCasesURLQueryParamsSchema + ); if (!validatedUrlParams) { return {}; diff --git a/x-pack/platform/plugins/shared/cases/public/components/attachments/file/types.ts b/x-pack/platform/plugins/shared/cases/public/components/attachments/file/types.ts index ba5f8efae063b..3bbe432d61938 100644 --- a/x-pack/platform/plugins/shared/cases/public/components/attachments/file/types.ts +++ b/x-pack/platform/plugins/shared/cases/public/components/attachments/file/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type * as rt from 'io-ts'; -import type { SingleFileAttachmentMetadataRt } from '../../../../common/types/domain'; +import type { z } from '@kbn/zod/v4'; +import type { SingleFileAttachmentMetadataSchema } from '../../../../common/types/domain'; -export type DownloadableFile = rt.TypeOf & { id: string }; +export type DownloadableFile = z.infer & { id: string }; diff --git a/x-pack/platform/plugins/shared/cases/public/containers/utils.ts b/x-pack/platform/plugins/shared/cases/public/containers/utils.ts index 578e90d9dd73f..b785e7a051822 100644 --- a/x-pack/platform/plugins/shared/cases/public/containers/utils.ts +++ b/x-pack/platform/plugins/shared/cases/public/containers/utils.ts @@ -6,19 +6,17 @@ */ import { isObject, transform, snakeCase, isEmpty } from 'lodash'; -import { fold } from 'fp-ts/Either'; -import { identity, pipe as v2Pipe } from 'fp-ts/function'; -import { pipe } from 'fp-ts/pipeable'; +import type { ZodType } from '@kbn/zod/v4'; import type { ToastInputFields } from '@kbn/core/public'; import { builderMap as customFieldsBuilder } from '../components/custom_fields/builder'; import { AttachmentType, - CaseRt, - CasesRt, - ConfigurationRt, - ConfigurationsRt, - UserActionsRt, + CaseSchema, + CasesSchema, + ConfigurationSchema, + ConfigurationsSchema, + UserActionsSchema, } from '../../common/types/domain'; import type { CasePatchRequest, @@ -29,11 +27,11 @@ import type { SingleCaseMetricsResponse, } from '../../common/types/api'; import { - CaseResolveResponseRt, - PatchCasesResponseRt, - CaseUserActionStatsResponseRt, - FindCasesContainingAllAlertsResponseRt, - SingleCaseMetricsResponseRt, + CaseResolveResponseSchema, + PatchCasesResponseSchema, + CaseUserActionStatsResponseSchema, + FindCasesContainingAllAlertsResponseSchema, + SingleCaseMetricsResponseSchema, } from '../../common/types/api'; import type { Case, @@ -44,7 +42,6 @@ import type { UserActions, } from '../../common/types/domain'; import { NO_ASSIGNEES_FILTERING_KEYWORD } from '../../common/constants'; -import { throwErrors } from '../../common/api'; import type { CaseUI, ExtendedFieldFilter, FilterOptions, UpdateByKey } from './types'; import * as i18n from './translations'; import type { CustomFieldFactoryFilterOption } from '../components/custom_fields/types'; @@ -59,55 +56,46 @@ export const covertToSnakeCase = (obj: Record) => export const createToasterPlainError = (message: string) => new ToasterError([message]); -export const decodeCaseResponse = (respCase?: Case) => - pipe(CaseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); +const decodeWithToasterError = (schema: ZodType, value: T): NonNullable => { + const result = schema.safeParse(value); + if (result.success) return result.data as NonNullable; + throw new ToasterError([ + result.error.issues + .map((issue) => `${issue.path.join('.') || ''}: ${issue.message}`) + .join(','), + ]); +}; + +export const decodeCaseResponse = (respCase?: Case) => decodeWithToasterError(CaseSchema, respCase); export const decodeCaseResolveResponse = (respCase?: CaseResolveResponse) => - pipe( - CaseResolveResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); + decodeWithToasterError(CaseResolveResponseSchema, respCase); export const decodeSingleCaseMetricsResponse = (respCase?: SingleCaseMetricsResponse) => - pipe( - SingleCaseMetricsResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); + decodeWithToasterError(SingleCaseMetricsResponseSchema, respCase); export const decodeCasesResponse = (respCase?: Cases) => - pipe(CasesRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + decodeWithToasterError(CasesSchema, respCase); export const decodeCasesWithUpdateSummaryResponse = (response?: CasesPatchResponse) => - pipe(PatchCasesResponseRt.decode(response), fold(throwErrors(createToasterPlainError), identity)); + decodeWithToasterError(PatchCasesResponseSchema, response); -export const decodeCaseConfigurationsResponse = (respCase?: Configurations) => { - return pipe( - ConfigurationsRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); -}; +export const decodeCaseConfigurationsResponse = (respCase?: Configurations) => + decodeWithToasterError(ConfigurationsSchema, respCase); export const decodeCaseConfigureResponse = (respCase?: Configuration) => - pipe(ConfigurationRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + decodeWithToasterError(ConfigurationSchema, respCase); export const decodeCaseUserActionsResponse = (respUserActions?: UserActions) => - pipe(UserActionsRt.decode(respUserActions), fold(throwErrors(createToasterPlainError), identity)); + decodeWithToasterError(UserActionsSchema, respUserActions); export const decodeCaseUserActionStatsResponse = ( caseUserActionsStats: CaseUserActionStatsResponse -) => - pipe( - CaseUserActionStatsResponseRt.decode(caseUserActionsStats), - fold(throwErrors(createToasterPlainError), identity) - ); +) => decodeWithToasterError(CaseUserActionStatsResponseSchema, caseUserActionsStats); export const decodeFindAllAttachedAlertsResponse = ( respCase?: FindCasesContainingAllAlertsResponse -) => - v2Pipe( - FindCasesContainingAllAlertsResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); +) => decodeWithToasterError(FindCasesContainingAllAlertsResponseSchema, respCase); export const valueToUpdateIsSettings = ( key: UpdateByKey['updateKey'], diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/add.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/add.test.ts index d564889ed6e40..be0c55346d8d9 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/add.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/add.test.ts @@ -40,7 +40,7 @@ describe('addComment', () => { await expect( // @ts-expect-error: excess attribute addComment({ comment: { ...comment, foo: 'bar' }, caseId }, clientArgs) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); it('should throw an error if the comment length is too long', async () => { @@ -49,7 +49,7 @@ describe('addComment', () => { await expect( addComment({ comment: { ...comment, comment: longComment }, caseId }, clientArgs) ).rejects.toThrow( - `Failed while adding a comment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + `Failed while adding a comment to case id: test-case error: Error: comment: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` ); }); @@ -57,7 +57,7 @@ describe('addComment', () => { await expect( addComment({ comment: { ...comment, comment: '' }, caseId }, clientArgs) ).rejects.toThrow( - 'Failed while adding a comment to case id: test-case error: Error: The comment field cannot be an empty string.' + 'Failed while adding a comment to case id: test-case error: Error: comment: The comment field cannot be an empty string.' ); }); @@ -65,7 +65,7 @@ describe('addComment', () => { await expect( addComment({ comment: { ...comment, comment: ' ' }, caseId }, clientArgs) ).rejects.toThrow( - 'Failed while adding a comment to case id: test-case error: Error: The comment field cannot be an empty string.' + 'Failed while adding a comment to case id: test-case error: Error: comment: The comment field cannot be an empty string.' ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/add.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/add.ts index c62455fe0e40e..fe0b7cb8b29cd 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/add.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/add.ts @@ -7,9 +7,9 @@ import { SavedObjectsUtils } from '@kbn/core/server'; -import { AttachmentRequestRtV2 } from '../../../common/types/api/attachment/v2'; +import { AttachmentRequestSchemaV2 } from '../../../common/types/api/attachment/v2'; import type { Case } from '../../../common/types/domain'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { CaseCommentModel } from '../../common/models'; import { createCaseError } from '../../common/error'; import type { CasesClientArgs } from '..'; @@ -37,7 +37,7 @@ export const addComment = async (addArgs: AddArgs, clientArgs: CasesClientArgs): } = clientArgs; try { - const query = decodeWithExcessOrThrow(AttachmentRequestRtV2)(comment); + const query = decodeWithExcessOrThrowZod(AttachmentRequestSchemaV2)(comment); await validateMaxUserActions({ caseId, userActionService, userActionsToAdd: 1 }); decodeCommentRequestV2( diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.test.ts index 16f803389f54a..4c37869509154 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.test.ts @@ -46,7 +46,7 @@ describe('addFile', () => { clientArgs, casesClient ) - ).rejects.toThrow(`Invalid value "undefined" supplied to "filename"`); + ).rejects.toThrow(`filename: Invalid input: expected string, received undefined`); }); it('throws an error if the mimeType is not part of the allowed mime types', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.ts index c65877d59a11e..de57c1281045d 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/add_file.ts @@ -8,7 +8,8 @@ import { SavedObjectsUtils } from '@kbn/core/server'; import type { Owner } from '../../../common/constants/types'; -import { FileAttachmentMetadataPayloadRt, type Case } from '../../../common/types/domain'; +import type { Case } from '../../../common/types/domain'; +import { FileAttachmentMetadataPayloadSchema } from '../../../common/types/domain'; import type { CasesClient, CasesClientArgs } from '..'; import type { AddFileArgs } from './types'; @@ -19,7 +20,7 @@ import { constructFileKindIdByOwner } from '../../../common/files'; import { Operations } from '../../authorization'; import { validateRegisteredAttachments } from './validators'; import { buildAttachmentRequestFromFileJSON } from '../utils'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; /** * Create a file attachment to a case. @@ -43,7 +44,7 @@ export const addFile = async ( let createdFile; try { - decodeWithExcessOrThrow(FileAttachmentMetadataPayloadRt)({ + decodeWithExcessOrThrowZod(FileAttachmentMetadataPayloadSchema)({ filename, mimeType, }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.test.ts index e207f3569353e..0eca1cb433ca4 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.test.ts @@ -41,7 +41,7 @@ describe('bulkCreate', () => { await expect( // @ts-expect-error: excess attribute bulkCreate({ attachments: [{ ...comment, foo: 'bar' }], caseId }, clientArgs) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); it(`throws error when attachments are more than ${MAX_BULK_CREATE_ATTACHMENTS}`, async () => { @@ -73,7 +73,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ attachments: [{ ...comment, comment: longComment }], caseId }, clientArgs) ).rejects.toThrow( - `Failed while bulk creating attachment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + `Failed while bulk creating attachment to case id: test-case error: Error: 0.comment: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` ); }); @@ -81,7 +81,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ attachments: [{ ...comment, comment: '' }], caseId }, clientArgs) ).rejects.toThrow( - 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + 'Failed while bulk creating attachment to case id: test-case error: Error: 0.comment: The comment field cannot be an empty string.' ); }); @@ -89,7 +89,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ attachments: [{ ...comment, comment: ' ' }], caseId }, clientArgs) ).rejects.toThrow( - 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + 'Failed while bulk creating attachment to case id: test-case error: Error: 0.comment: The comment field cannot be an empty string.' ); }); }); @@ -106,7 +106,7 @@ describe('bulkCreate', () => { clientArgs ) ).rejects.toThrow( - `Failed while bulk creating attachment to case id: test-case error: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + `Failed while bulk creating attachment to case id: test-case error: Error: 0.comment: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` ); }); @@ -114,7 +114,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ attachments: [{ ...actionComment, comment: '' }], caseId }, clientArgs) ).rejects.toThrow( - 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + 'Failed while bulk creating attachment to case id: test-case error: Error: 0.comment: The comment field cannot be an empty string.' ); }); @@ -122,7 +122,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ attachments: [{ ...actionComment, comment: ' ' }], caseId }, clientArgs) ).rejects.toThrow( - 'Failed while bulk creating attachment to case id: test-case error: Error: The comment field cannot be an empty string.' + 'Failed while bulk creating attachment to case id: test-case error: Error: 0.comment: The comment field cannot be an empty string.' ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.ts index 42c09c38e148e..e302efa174636 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_create.ts @@ -8,9 +8,9 @@ import { SavedObjectsUtils } from '@kbn/core/server'; import type { AttachmentRequestV2 } from '../../../common/types/api'; -import { BulkCreateAttachmentsRequestRtV2 } from '../../../common/types/api/attachment/v2'; +import { BulkCreateAttachmentsRequestSchemaV2 } from '../../../common/types/api/attachment/v2'; import type { Case } from '../../../common/types/domain'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { CaseCommentModel } from '../../common/models'; import { createCaseError } from '../../common/error'; @@ -40,7 +40,7 @@ export const bulkCreate = async ( } = clientArgs; try { - decodeWithExcessOrThrow(BulkCreateAttachmentsRequestRtV2)(attachments); + decodeWithExcessOrThrowZod(BulkCreateAttachmentsRequestSchemaV2)(attachments); await validateMaxUserActions({ caseId, userActionService, diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.test.ts index 4a34153400048..7c07c8b32a488 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.test.ts @@ -28,7 +28,7 @@ describe('bulk_delete', () => { await expect( bulkDeleteFileAttachments({ caseId: 'mock-id', fileIds }, clientArgs, casesClient) ).rejects.toThrowError( - 'Failed to delete file attachments for case: mock-id: Error: The length of the field ids is too long. Array must be of length <= 10' + 'Failed to delete file attachments for case: mock-id: Error: ids: The length of the field ids is too long. Array must be of length <= 10' ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.ts index 1d1093beef77e..3192465502cfa 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_delete.ts @@ -13,14 +13,14 @@ import type { Logger } from '@kbn/core/server'; import type { File, FileJSON } from '@kbn/files-plugin/common'; import type { FileServiceStart } from '@kbn/files-plugin/server'; import { FileNotFoundError } from '@kbn/files-plugin/server/file_service/errors'; -import { BulkDeleteFileAttachmentsRequestRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { BulkDeleteFileAttachmentsRequestSchema } from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { MAX_CONCURRENT_SEARCHES } from '../../../common/constants'; import type { CasesClientArgs } from '../types'; import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; import type { BulkDeleteFileArgs } from './types'; -import { CaseFileMetadataForDeletionRt } from '../../../common/files'; +import { CaseFileMetadataForDeletionSchema } from '../../../common/files'; import type { CasesClient } from '../client'; import { createFileEntities, deleteFiles } from '../files'; @@ -38,7 +38,9 @@ export const bulkDeleteFileAttachments = async ( } = clientArgs; try { - const request = decodeWithExcessOrThrow(BulkDeleteFileAttachmentsRequestRt)({ ids: fileIds }); + const request = decodeWithExcessOrThrowZod(BulkDeleteFileAttachmentsRequestSchema)({ + ids: fileIds, + }); await casesClient.cases.resolve({ id: caseId, includeComments: false }); @@ -144,10 +146,9 @@ const getFiles = async ({ const files = retrieveFilesIgnoringNotFound(fileSettleResults, fileIds, logger); const [validFiles, invalidFiles] = partition(files, (file) => { + const parsed = CaseFileMetadataForDeletionSchema.safeParse(file.data.meta); return ( - CaseFileMetadataForDeletionRt.is(file.data.meta) && - file.data.meta.caseIds.length === 1 && - file.data.meta.caseIds.includes(caseId) + parsed.success && parsed.data.caseIds.length === 1 && parsed.data.caseIds.includes(caseId) ); }) as [File[], File[]]; diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.test.ts index 8eee6af637c23..9968fd593a89b 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.test.ts @@ -40,7 +40,7 @@ describe('bulkGet', () => { casesClient ) ).rejects.toThrow( - `Error: The length of the field ids is too long. Array must be of length <= ${MAX_BULK_GET_ATTACHMENTS}.` + `Error: ids: The length of the field ids is too long. Array must be of length <= ${MAX_BULK_GET_ATTACHMENTS}.` ); }); @@ -48,7 +48,7 @@ describe('bulkGet', () => { await expect( bulkGet({ savedObjectIds: [], caseID: '123', mode: 'legacy' }, clientArgs, casesClient) ).rejects.toThrow( - 'Error: The length of the field ids is too short. Array must be of length >= 1.' + 'Error: ids: The length of the field ids is too short. Array must be of length >= 1.' ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts index 36a56481a9170..af89e0a447748 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/bulk_get.ts @@ -11,10 +11,10 @@ import type { BulkGetAttachmentsResponseV2, } from '../../../common/types/api'; import { - BulkGetAttachmentsRequestRt, - BulkGetAttachmentsResponseRt, - BulkGetAttachmentsResponseRtV2, + BulkGetAttachmentsRequestSchema, + BulkGetAttachmentsResponseSchema, } from '../../../common/types/api'; +import { BulkGetAttachmentsResponseSchemaV2 } from '../../../common/types/api/attachment/v2'; import type { AttachmentAttributes, AttachmentAttributesV2 } from '../../../common/types/domain'; import { flattenAttachmentSavedObjects } from '../../common/utils'; import { createCaseError, generateCaseErrorResponse } from '../../common/error'; @@ -25,7 +25,7 @@ import type { BulkOptionalAttributes, OptionalAttributes } from '../../services/ import type { CasesClient } from '../client'; import type { AttachmentSavedObject, SOWithErrors } from '../../common/types'; import { partitionByCaseAssociation } from '../../common/partitioning'; -import { decodeOrThrow, decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod, decodeWithExcessOrThrowZod } from '../../common/runtime_types'; type AttachmentSavedObjectWithErrors = Array>; @@ -44,7 +44,9 @@ export async function bulkGet( } = clientArgs; try { - const request = decodeWithExcessOrThrow(BulkGetAttachmentsRequestRt)({ ids: savedObjectIds }); + const request = decodeWithExcessOrThrowZod(BulkGetAttachmentsRequestSchema)({ + ids: savedObjectIds, + }); // perform an authorization check for the case await casesClient.cases.resolve({ id: caseID }); @@ -72,9 +74,13 @@ export async function bulkGet( errors, }; if (mode === 'legacy') { - return decodeOrThrow(BulkGetAttachmentsResponseRt)(res); + return decodeOrThrowZod(BulkGetAttachmentsResponseSchema)( + res + ) as unknown as BulkGetAttachmentsResponseV2; } - return decodeOrThrow(BulkGetAttachmentsResponseRtV2)(res); + return decodeOrThrowZod(BulkGetAttachmentsResponseSchemaV2)( + res + ) as BulkGetAttachmentsResponseV2; } catch (error) { throw createCaseError({ message: `Failed to bulk get attachments for case id: ${caseID}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts index e722a33b61b0f..6cd79dbdcaea3 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/delete.ts @@ -10,7 +10,7 @@ import Boom from '@hapi/boom'; import { isLegacyAttachmentRequest } from '../../../common/utils/attachments'; import type { AlertAttachmentPayload } from '../../../common/types/domain'; import { UserActionActions, UserActionTypes } from '../../../common/types/domain'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { getAlertInfoFromComments, isCommentRequestTypeAlert } from '../../common/utils'; import type { CasesClientArgs } from '../types'; @@ -18,7 +18,7 @@ import { createCaseError } from '../../common/error'; import { Operations } from '../../authorization'; import type { DeleteAllArgs, DeleteArgs } from './types'; import type { AttachmentRequestV2 } from '../../../common/types/api'; -import { AttachmentRequestRtV2 } from '../../../common/types/api'; +import { AttachmentRequestSchemaV2 } from '../../../common/types/api/attachment/v2'; /** * Delete all comments for a case. @@ -138,7 +138,9 @@ export async function deleteComment( // we only want to store the fields related to the original request of the attachment, not fields like // created_at etc. So we'll use the decode to strip off the other fields. This is necessary because we don't know // what type of attachment this is. Depending on the type it could have various fields. - const attachmentRequestAttributes = decodeOrThrow(AttachmentRequestRtV2)(attachment.attributes); + const attachmentRequestAttributes = decodeOrThrowZod(AttachmentRequestSchemaV2)( + attachment.attributes + ) as AttachmentRequestV2; await userActionService.creator.createUserAction({ userAction: { diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/get.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/get.test.ts index bcaafc761bded..d71a054628ec6 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/get.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/get.test.ts @@ -48,7 +48,7 @@ describe('get', () => { clientArgs ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to find comments case id: mock-id: Error: invalid keys \\"foo\\""` + `"Failed to find comments case id: mock-id: Error: Excess keys are not allowed"` ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts index 89ad96b59f13f..05df7ea29b450 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/get.ts @@ -15,17 +15,21 @@ import type { import { AttachmentType } from '../../../common'; import type { DocumentResponse, AttachmentsFindResponse } from '../../../common/types/api'; import { - DocumentResponseRt, - FindAttachmentsQueryParamsRt, - AttachmentsFindResponseRt, + DocumentResponseSchema, + FindAttachmentsQueryParamsSchema, } from '../../../common/types/api'; +import { AttachmentsFindResponseSchemaV2 } from '../../../common/types/api/attachment/v2'; +import { + AttachmentSchemaV2, + AttachmentsSchemaV2, +} from '../../../common/types/domain/attachment/v2'; import type { CasesClient } from '../client'; import type { CasesClientArgs } from '../types'; import type { FindCommentsArgs, GetAllDocumentsAttachedToCase, GetAllArgs, GetArgs } from './types'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT } from '../../../common/constants'; -import { decodeOrThrow, decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod, decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { defaultSortField, transformComments, @@ -37,7 +41,6 @@ import { createCaseError } from '../../common/error'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../routes/api'; import { buildFilter, combineFilters } from '../utils'; import { Operations } from '../../authorization'; -import { AttachmentRtV2, AttachmentsRtV2 } from '../../../common/types/domain'; const normalizeDocumentResponse = ( documents: Array> @@ -102,7 +105,7 @@ export const getAllDocumentsAttachedToCase = async ( const res = normalizeDocumentResponse(documents); - return decodeOrThrow(DocumentResponseRt)(res); + return decodeOrThrowZod(DocumentResponseSchema)(res) as DocumentResponse; } catch (error) { throw createCaseError({ message: `Failed to get documents attached to case id: ${caseId}: ${error}`, @@ -126,7 +129,9 @@ export async function find( } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(FindAttachmentsQueryParamsRt)(findQueryParams); + const queryParams = decodeWithExcessOrThrowZod(FindAttachmentsQueryParamsSchema)( + findQueryParams + ); const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = await authorization.getAuthorizationFilter(Operations.findComments); @@ -162,7 +167,7 @@ export async function find( const res = transformComments(theComments); - return decodeOrThrow(AttachmentsFindResponseRt)(res); + return decodeOrThrowZod(AttachmentsFindResponseSchemaV2)(res) as AttachmentsFindResponse; } catch (error) { throw createCaseError({ message: `Failed to find comments case id: ${caseID}: ${error}`, @@ -198,7 +203,7 @@ export async function get( const res = flattenAttachmentSavedObject(comment); - return decodeOrThrow(AttachmentRtV2)(res); + return decodeOrThrowZod(AttachmentSchemaV2)(res) as AttachmentV2; } catch (error) { throw createCaseError({ message: `Failed to get comment case id: ${caseID} attachment id: ${savedObjectId}: ${error}`, @@ -241,7 +246,7 @@ export async function getAll( const res = flattenAttachmentSavedObjects(comments.saved_objects); - return decodeOrThrow(AttachmentsRtV2)(res); + return decodeOrThrowZod(AttachmentsSchemaV2)(res) as AttachmentsV2; } catch (error) { throw createCaseError({ message: `Failed to get all comments case id: ${caseID}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/update.test.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/update.test.ts index 7b0c3c0c99c42..f456d819910f8 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/update.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/update.test.ts @@ -46,7 +46,7 @@ describe('update', () => { await expect( update({ updateRequest: { ...updateComment, comment: longComment }, caseID }, clientArgs) ).rejects.toThrow( - `Failed to patch comment case id: test-case: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + `Failed to patch comment case id: test-case: Error: comment: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` ); }); @@ -54,7 +54,7 @@ describe('update', () => { await expect( update({ updateRequest: { ...updateComment, comment: '' }, caseID }, clientArgs) ).rejects.toThrow( - 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + 'Failed to patch comment case id: test-case: Error: comment: The comment field cannot be an empty string.' ); }); @@ -62,7 +62,7 @@ describe('update', () => { await expect( update({ updateRequest: { ...updateComment, comment: ' ' }, caseID }, clientArgs) ).rejects.toThrow( - 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + 'Failed to patch comment case id: test-case: Error: comment: The comment field cannot be an empty string.' ); }); @@ -93,7 +93,7 @@ describe('update', () => { clientArgs ) ).rejects.toThrow( - `Failed to patch comment case id: test-case: Error: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` + `Failed to patch comment case id: test-case: Error: comment: The length of the comment is too long. The maximum length is ${MAX_COMMENT_LENGTH}.` ); }); @@ -101,7 +101,7 @@ describe('update', () => { await expect( update({ updateRequest: { ...updateActionComment, comment: '' }, caseID }, clientArgs) ).rejects.toThrow( - 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + 'Failed to patch comment case id: test-case: Error: comment: The comment field cannot be an empty string.' ); }); @@ -109,7 +109,7 @@ describe('update', () => { await expect( update({ updateRequest: { ...updateActionComment, comment: ' ' }, caseID }, clientArgs) ).rejects.toThrow( - 'Failed to patch comment case id: test-case: Error: The comment field cannot be an empty string.' + 'Failed to patch comment case id: test-case: Error: comment: The comment field cannot be an empty string.' ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/attachments/update.ts b/x-pack/platform/plugins/shared/cases/server/client/attachments/update.ts index 9f02984d41e7d..0ad5e8045d68b 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/attachments/update.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/attachments/update.ts @@ -7,12 +7,12 @@ import Boom from '@hapi/boom'; -import { AttachmentPatchRequestRtV2 } from '../../../common/types/api'; +import { AttachmentPatchRequestSchemaV2 } from '../../../common/types/api/attachment/v2'; import { CaseCommentModel } from '../../common/models'; import { createCaseError } from '../../common/error'; import { isCommentRequestTypeExternalReference } from '../../../common/utils/attachments'; import type { Case } from '../../../common/types/domain'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; import type { CasesClientArgs } from '..'; import { decodeCommentRequestV2 } from '../utils'; @@ -44,7 +44,7 @@ export async function update( id: queryCommentId, version: queryCommentVersion, ...queryRestAttributes - } = decodeWithExcessOrThrow(AttachmentPatchRequestRtV2)(queryParams); + } = decodeWithExcessOrThrowZod(AttachmentPatchRequestSchemaV2)(queryParams); await validateMaxUserActions({ caseId: caseID, userActionService, diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts index 036064daccac3..4f58c5c7fd8a8 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.test.ts @@ -620,7 +620,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ assignees }) }, clientArgs, casesClientMock) ).rejects.toThrow( - `Failed to bulk create cases: Error: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_PER_CASE}.` + `Failed to bulk create cases: Error: cases.0.assignees: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_PER_CASE}.` ); }); @@ -641,7 +641,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ foo: 'bar' }) }, clientArgs, casesClientMock) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to bulk create cases: Error: invalid keys \\"foo\\""` + `"Failed to bulk create cases: Error: Excess keys are not allowed"` ); }); }); @@ -673,7 +673,7 @@ describe('bulkCreate', () => { casesClientMock ) ).rejects.toThrow( - `Failed to bulk create cases: Error: The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}.` + `Failed to bulk create cases: Error: cases.0.title: The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}.` ); }); @@ -681,7 +681,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ title: '' }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The title field cannot be an empty string.' + 'Failed to bulk create cases: Error: cases.0.title: The title field cannot be an empty string.' ); }); @@ -689,7 +689,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ title: ' ' }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The title field cannot be an empty string.' + 'Failed to bulk create cases: Error: cases.0.title: The title field cannot be an empty string.' ); }); @@ -728,7 +728,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ description }) }, clientArgs, casesClientMock) ).rejects.toThrow( - `Failed to bulk create cases: Error: The length of the description is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}.` + `Failed to bulk create cases: Error: cases.0.description: The length of the description is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}.` ); }); @@ -736,7 +736,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ description: '' }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The description field cannot be an empty string.' + 'Failed to bulk create cases: Error: cases.0.description: The description field cannot be an empty string.' ); }); @@ -744,7 +744,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ description: ' ' }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The description field cannot be an empty string.' + 'Failed to bulk create cases: Error: cases.0.description: The description field cannot be an empty string.' ); }); @@ -784,7 +784,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ tags }) }, clientArgs, casesClientMock) ).rejects.toThrow( - `Failed to bulk create cases: Error: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_PER_CASE}.` + `Failed to bulk create cases: Error: cases.0.tags: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_PER_CASE}.` ); }); @@ -792,7 +792,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ tags: [''] }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The tag field cannot be an empty string.' + 'Failed to bulk create cases: Error: cases.0.tags.0: The tag field cannot be an empty string.' ); }); @@ -800,7 +800,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ tags: [' '] }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The tag field cannot be an empty string.' + 'Failed to bulk create cases: Error: cases.0.tags.0: The tag field cannot be an empty string.' ); }); @@ -812,7 +812,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ tags: [tag] }) }, clientArgs, casesClientMock) ).rejects.toThrow( - `Failed to bulk create cases: Error: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` + `Failed to bulk create cases: Error: cases.0.tags.0: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` ); }); @@ -849,7 +849,7 @@ describe('bulkCreate', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The length of the category is too long.' + 'Failed to bulk create cases: Error: cases.0.category: The length of the category is too long.' ); }); @@ -857,7 +857,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ category: '' }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The category field cannot be an empty string.,Invalid value "" supplied to "cases,category"' + 'Failed to bulk create cases: Error: cases.0.category: The category field cannot be an empty string.' ); }); @@ -865,7 +865,7 @@ describe('bulkCreate', () => { await expect( bulkCreate({ cases: getCases({ category: ' ' }) }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to bulk create cases: Error: The category field cannot be an empty string.,Invalid value " " supplied to "cases,category"' + 'Failed to bulk create cases: Error: cases.0.category: The category field cannot be an empty string.' ); }); @@ -1050,7 +1050,7 @@ describe('bulkCreate', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to bulk create cases: Error: The length of the field customFields is too long. Array must be of length <= 10."` + `"Failed to bulk create cases: Error: cases.0.customFields: The length of the field customFields is too long. Array must be of length <= 10."` ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts index 7b7125621c87c..42729fe31fb6c 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_create.ts @@ -13,7 +13,7 @@ import { SavedObjectsUtils } from '@kbn/core/server'; import type { Case, CustomFieldsConfiguration, User } from '../../../common/types/domain'; import { CaseSeverity, UserActionTypes } from '../../../common/types/domain'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { Operations } from '../../authorization'; import { createCaseError, isSODecoratedError, isSOError } from '../../common/error'; @@ -26,7 +26,10 @@ import type { BulkCreateCasesResponse, CasePostRequest, } from '../../../common/types/api'; -import { BulkCreateCasesResponseRt, BulkCreateCasesRequestRt } from '../../../common/types/api'; +import { + BulkCreateCasesResponseSchema, + BulkCreateCasesRequestSchema, +} from '../../../common/types/api'; import { validateCustomFields } from './validators'; import { normalizeCreateCaseRequest } from './utils'; import type { BulkCreateCasesArgs } from '../../services/cases/types'; @@ -52,7 +55,9 @@ export const bulkCreate = async ( } = clientArgs; try { - const decodedData = decodeWithExcessOrThrow(BulkCreateCasesRequestRt)(data); + const decodedData = decodeWithExcessOrThrowZod(BulkCreateCasesRequestSchema)( + data + ) as BulkCreateCasesRequest; const configurations = await casesClient.configure.get(); const customFieldsConfigurationMap: Map = new Map( @@ -157,7 +162,9 @@ export const bulkCreate = async ( }) ); - const createdCasesResponse = decodeOrThrow(BulkCreateCasesResponseRt)({ cases: res }); + const createdCasesResponse = decodeOrThrowZod(BulkCreateCasesResponseSchema)({ + cases: res, + }) as BulkCreateCasesResponse; createdCasesResponse.cases.forEach((createdCase) => { clientArgs.casesEventBus?.emitCaseCreated(clientArgs.request, { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts index 921c4aff11f6a..14eb68090fc5f 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.test.ts @@ -61,13 +61,13 @@ describe('bulkGet', () => { await expect( bulkGet({ ids: Array(MAX_BULK_GET_CASES + 1).fill('foobar') }, clientArgs) ).rejects.toThrow( - `Error: The length of the field ids is too long. Array must be of length <= ${MAX_BULK_GET_CASES}.` + `Error: ids: The length of the field ids is too long. Array must be of length <= ${MAX_BULK_GET_CASES}.` ); }); it('throws when trying to fetch zero cases', async () => { await expect(bulkGet({ ids: [] }, clientArgs)).rejects.toThrow( - 'Error: The length of the field ids is too short. Array must be of length >= 1.' + 'Error: ids: The length of the field ids is too short. Array must be of length >= 1.' ); }); @@ -78,7 +78,7 @@ describe('bulkGet', () => { { ids: ['1'], foo: 'bar' }, clientArgs ) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); it('constructs the case error correctly', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts index 90a6e2de13621..ae952128bf369 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_get.ts @@ -9,8 +9,8 @@ import { partition } from 'lodash'; import type { CaseAttributes } from '../../../common/types/domain'; import type { CasesBulkGetRequest, CasesBulkGetResponse } from '../../../common/types/api'; -import { CasesBulkGetResponseRt, CasesBulkGetRequestRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { CasesBulkGetResponseSchema, CasesBulkGetRequestSchema } from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError, generateCaseErrorResponse } from '../../common/error'; import { flattenCaseSavedObject } from '../../common/utils'; import type { CasesClientArgs } from '../types'; @@ -34,7 +34,7 @@ export const bulkGet = async ( } = clientArgs; try { - const request = decodeWithExcessOrThrow(CasesBulkGetRequestRt)(params); + const request = decodeWithExcessOrThrowZod(CasesBulkGetRequestSchema)(params); const cases = await caseService.getCases({ caseIds: request.ids }); @@ -71,7 +71,7 @@ export const bulkGet = async ( const errors = constructErrors(soBulkGetErrors, unauthorizedCases); const res = { cases: flattenedCases, errors }; - return decodeOrThrow(CasesBulkGetResponseRt)(res); + return decodeOrThrowZod(CasesBulkGetResponseSchema)(res) as CasesBulkGetResponse; } catch (error) { const ids = params.ids ?? []; throw createCaseError({ diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts index d8f28aaa93828..e6243689b3428 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.test.ts @@ -344,7 +344,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: invalid keys \\"foo\\""` + `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: Excess keys are not allowed"` ); }); @@ -366,7 +366,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the field assignees is too long. Array must be of length <= 10.' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.assignees: The length of the field assignees is too long. Array must be of length <= 10.' ); }); @@ -533,7 +533,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the category is too long. The maximum length is ${MAX_CATEGORY_LENGTH}.,Invalid value \"A very long category with more than fifty characters!\" supplied to \"cases,category\"` + `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.category: The length of the category is too long. The maximum length is ${MAX_CATEGORY_LENGTH}.` ); }); @@ -553,7 +553,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The category field cannot be an empty string.,Invalid value "" supplied to "cases,category"' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.category: The category field cannot be an empty string.' ); }); @@ -573,7 +573,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The category field cannot be an empty string.,Invalid value " " supplied to "cases,category"' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.category: The category field cannot be an empty string.' ); }); @@ -670,7 +670,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}.` + `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.title: The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}.` ); }); @@ -690,7 +690,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The title field cannot be an empty string.' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.title: The title field cannot be an empty string.' ); }); @@ -710,7 +710,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The title field cannot be an empty string.' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.title: The title field cannot be an empty string.' ); }); @@ -810,7 +810,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the description is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}.` + `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.description: The length of the description is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}.` ); }); @@ -830,7 +830,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The description field cannot be an empty string.' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.description: The description field cannot be an empty string.' ); }); @@ -850,7 +850,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The description field cannot be an empty string.' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.description: The description field cannot be an empty string.' ); }); @@ -1126,7 +1126,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_PER_CASE}.` + `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.tags: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_PER_CASE}.` ); }); @@ -1150,7 +1150,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` + `Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.tags.0: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` ); }); @@ -1170,7 +1170,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The tag field cannot be an empty string.' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.tags.0: The tag field cannot be an empty string.' ); }); @@ -1190,7 +1190,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: The tag field cannot be an empty string.' + 'Failed to update case, ids: [{"id":"mock-id-1","version":"WzAsMV0="}]: Error: cases.0.tags.0: The tag field cannot be an empty string.' ); }); @@ -1462,7 +1462,7 @@ describe('update', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: The length of the field customFields is too long. Array must be of length <= 10."` + `"Failed to update case, ids: [{\\"id\\":\\"mock-id-1\\",\\"version\\":\\"WzAsMV0=\\"}]: Error: cases.0.customFields: The length of the field customFields is too long. Array must be of length <= 10."` ); }); @@ -1677,7 +1677,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Error: The length of the field cases is too long. Array must be of length <= 100.' + 'Error: cases: The length of the field cases is too long. Array must be of length <= 100.' ); }); @@ -1691,7 +1691,7 @@ describe('update', () => { casesClientMock ) ).rejects.toThrow( - 'Error: The length of the field cases is too short. Array must be of length >= 1.' + 'Error: cases: The length of the field cases is too short. Array must be of length >= 1.' ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts index 3090ef6f2f9ae..1ef90ac4b5b0c 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/bulk_update.ts @@ -31,7 +31,7 @@ import type { CasesPatchResponse, CaseWithUpdateSummary, } from '../../../common/types/api'; -import { PatchCasesResponseRt, CasesPatchRequestRt } from '../../../common/types/api'; +import { PatchCasesResponseSchema, CasesPatchRequestSchema } from '../../../common/types/api'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -58,7 +58,7 @@ import { import { LICENSING_CASE_ASSIGNMENT_FEATURE } from '../../common/constants'; import type { LicensingService } from '../../services/licensing'; import type { CaseSavedObjectTransformed } from '../../common/types/case'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import type { CaseAttributes, User, @@ -427,7 +427,9 @@ export const bulkUpdate = async ( } = clientArgs; try { - const rawQuery = decodeWithExcessOrThrow(CasesPatchRequestRt)(cases); + const rawQuery = decodeWithExcessOrThrowZod(CasesPatchRequestSchema)( + cases + ) as CasesPatchRequest; const query = emptyCasesAssigneesSanitizer(rawQuery); const caseIds = query.cases.map((q) => q.id); const myCases = await caseService.getCases({ @@ -658,7 +660,9 @@ export const bulkUpdate = async ( await notificationService.bulkNotifyAssignees(casesAndAssigneesToNotifyForAssignment); - const updatedCasesResponse = decodeOrThrow(PatchCasesResponseRt)(returnUpdatedCase); + const updatedCasesResponse = decodeOrThrowZod(PatchCasesResponseSchema)( + returnUpdatedCase + ) as CasesPatchResponse; const updatedFieldsByCaseId = casesToUpdate.reduce>( (acc, { updateReq }) => { // Keep first occurrence for duplicate ids handling. diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts index f73705116d860..b6b645fd9c5b6 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/create.test.ts @@ -115,7 +115,7 @@ describe('create', () => { const assignees = Array(MAX_ASSIGNEES_PER_CASE + 1).fill({ uid: 'foo' }); await expect(create({ ...theCase, assignees }, clientArgs, casesClientMock)).rejects.toThrow( - `Failed to create case: Error: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_PER_CASE}.` + `Failed to create case: Error: assignees: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_PER_CASE}.` ); }); @@ -202,7 +202,7 @@ describe('create', () => { // @ts-expect-error foo is an invalid field create({ ...theCase, foo: 'bar' }, clientArgs, casesClientMock) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to create case: Error: invalid keys \\"foo\\""` + `"Failed to create case: Error: Excess keys are not allowed"` ); }); }); @@ -233,20 +233,22 @@ describe('create', () => { casesClientMock ) ).rejects.toThrow( - `Failed to create case: Error: The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}.` + `Failed to create case: Error: title: The length of the title is too long. The maximum length is ${MAX_TITLE_LENGTH}.` ); }); it('should throw an error if the title is an empty string', async () => { await expect(create({ ...theCase, title: '' }, clientArgs, casesClientMock)).rejects.toThrow( - 'Failed to create case: Error: The title field cannot be an empty string.' + 'Failed to create case: Error: title: The title field cannot be an empty string.' ); }); it('should throw an error if the title is a string with empty characters', async () => { await expect( create({ ...theCase, title: ' ' }, clientArgs, casesClientMock) - ).rejects.toThrow('Failed to create case: Error: The title field cannot be an empty string.'); + ).rejects.toThrow( + 'Failed to create case: Error: title: The title field cannot be an empty string.' + ); }); it('should trim title', async () => { @@ -288,7 +290,7 @@ describe('create', () => { await expect( create({ ...theCase, description }, clientArgs, casesClientMock) ).rejects.toThrow( - `Failed to create case: Error: The length of the description is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}.` + `Failed to create case: Error: description: The length of the description is too long. The maximum length is ${MAX_DESCRIPTION_LENGTH}.` ); }); @@ -296,7 +298,7 @@ describe('create', () => { await expect( create({ ...theCase, description: '' }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to create case: Error: The description field cannot be an empty string.' + 'Failed to create case: Error: description: The description field cannot be an empty string.' ); }); @@ -304,7 +306,7 @@ describe('create', () => { await expect( create({ ...theCase, description: ' ' }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to create case: Error: The description field cannot be an empty string.' + 'Failed to create case: Error: description: The description field cannot be an empty string.' ); }); @@ -349,20 +351,22 @@ describe('create', () => { const tags = Array(MAX_TAGS_PER_CASE + 1).fill('foo'); await expect(create({ ...theCase, tags }, clientArgs, casesClientMock)).rejects.toThrow( - `Failed to create case: Error: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_PER_CASE}.` + `Failed to create case: Error: tags: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_PER_CASE}.` ); }); it('should throw an error if the tags array has empty string', async () => { await expect(create({ ...theCase, tags: [''] }, clientArgs, casesClientMock)).rejects.toThrow( - 'Failed to create case: Error: The tag field cannot be an empty string.' + 'Failed to create case: Error: tags.0: The tag field cannot be an empty string.' ); }); it('should throw an error if the tags array has string with empty characters', async () => { await expect( create({ ...theCase, tags: [' '] }, clientArgs, casesClientMock) - ).rejects.toThrow('Failed to create case: Error: The tag field cannot be an empty string.'); + ).rejects.toThrow( + 'Failed to create case: Error: tags.0: The tag field cannot be an empty string.' + ); }); it('should throw an error if the tag length is too long', async () => { @@ -373,7 +377,7 @@ describe('create', () => { await expect( create({ ...theCase, tags: [tag] }, clientArgs, casesClientMock) ).rejects.toThrow( - `Failed to create case: Error: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` + `Failed to create case: Error: tags.0: The length of the tag is too long. The maximum length is ${MAX_LENGTH_PER_TAG}.` ); }); @@ -411,14 +415,16 @@ describe('create', () => { clientArgs, casesClientMock ) - ).rejects.toThrow('Failed to create case: Error: The length of the category is too long.'); + ).rejects.toThrow( + 'Failed to create case: Error: category: The length of the category is too long.' + ); }); it('should throw an error if the category is an empty string', async () => { await expect( create({ ...theCase, category: '' }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to create case: Error: The category field cannot be an empty string.,Invalid value "" supplied to "category"' + 'Failed to create case: Error: category: The category field cannot be an empty string.' ); }); @@ -426,7 +432,7 @@ describe('create', () => { await expect( create({ ...theCase, category: ' ' }, clientArgs, casesClientMock) ).rejects.toThrow( - 'Failed to create case: Error: The category field cannot be an empty string.,Invalid value " " supplied to "category"' + 'Failed to create case: Error: category: The category field cannot be an empty string.' ); }); @@ -596,7 +602,7 @@ describe('create', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to create case: Error: The length of the field customFields is too long. Array must be of length <= 10."` + `"Failed to create case: Error: customFields: The length of the field customFields is too long. Array must be of length <= 10."` ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts index 7a667e7818d2b..783787472b8d4 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/create.ts @@ -9,8 +9,8 @@ import Boom from '@hapi/boom'; import { SavedObjectsUtils } from '@kbn/core/server'; import type { Case } from '../../../common/types/domain'; -import { CaseSeverity, UserActionTypes, CaseRt } from '../../../common/types/domain'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { CaseSeverity, UserActionTypes, CaseSchema } from '../../../common/types/domain'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; @@ -19,7 +19,7 @@ import type { CasesClient, CasesClientArgs } from '..'; import { LICENSING_CASE_ASSIGNMENT_FEATURE } from '../../common/constants'; import type { Owner } from '../../../common/constants/types'; import type { CasePostRequest } from '../../../common/types/api'; -import { CasePostRequestRt } from '../../../common/types/api'; +import { CasePostRequestSchema } from '../../../common/types/api'; import { validateCustomFields } from './validators'; import { emptyCaseAssigneesSanitizer } from './sanitizers'; import { normalizeCreateCaseRequest } from './utils'; @@ -49,7 +49,7 @@ export const create = async ( } = clientArgs; try { - const rawQuery = decodeWithExcessOrThrow(CasePostRequestRt)(data); + const rawQuery = decodeWithExcessOrThrowZod(CasePostRequestSchema)(data) as CasePostRequest; const query = emptyCaseAssigneesSanitizer(rawQuery); const configurations = await casesClient.configure.get({ owner: data.owner }); const customFieldsConfiguration = configurations[0]?.customFields; @@ -174,7 +174,7 @@ export const create = async ( savedObject: newCase, }); - const createdCase = decodeOrThrow(CaseRt)(res); + const createdCase = decodeOrThrowZod(CaseSchema)(res) as Case; clientArgs.casesEventBus?.emitCaseCreated(clientArgs.request, { caseId: createdCase.id, diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/delete.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/delete.ts index c01e139fa9d15..a74ed5edcb02e 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/delete.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/delete.ts @@ -10,8 +10,8 @@ import { chunk } from 'lodash'; import type { SavedObjectsBulkDeleteObject } from '@kbn/core/server'; import type { FileServiceStart } from '@kbn/files-plugin/server'; import type { CasesDeleteRequest } from '../../../common/types/api'; -import { CasesDeleteRequestRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { CasesDeleteRequestSchema } from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, @@ -39,7 +39,7 @@ export async function deleteCases( } = clientArgs; try { - const caseIds = decodeWithExcessOrThrow(CasesDeleteRequestRt)(ids); + const caseIds = decodeWithExcessOrThrowZod(CasesDeleteRequestSchema)(ids) as CasesDeleteRequest; const cases = await caseService.getCases({ caseIds }); const entities = new Map(); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/find.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/find.test.ts index e94ce29c87d8c..12703222d1072 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/find.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/find.test.ts @@ -154,7 +154,7 @@ describe('find', () => { // @ts-expect-error foo is an invalid field find({ ...findRequest, foo: 'bar' }, clientArgs) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to find cases: {\\"search\\":\\"sample_text\\",\\"searchFields\\":[\\"title\\",\\"description\\",\\"incremental_id.text\\"],\\"severity\\":\\"low\\",\\"assignees\\":[],\\"reporters\\":[],\\"status\\":\\"open\\",\\"tags\\":[],\\"owner\\":[],\\"sortField\\":\\"createdAt\\",\\"sortOrder\\":\\"desc\\",\\"customFields\\":{},\\"foo\\":\\"bar\\"}: Error: invalid keys \\"foo\\""` + `"Failed to find cases: {\\"search\\":\\"sample_text\\",\\"searchFields\\":[\\"title\\",\\"description\\",\\"incremental_id.text\\"],\\"severity\\":\\"low\\",\\"assignees\\":[],\\"reporters\\":[],\\"status\\":\\"open\\",\\"tags\\":[],\\"owner\\":[],\\"sortField\\":\\"createdAt\\",\\"sortOrder\\":\\"desc\\",\\"customFields\\":{},\\"foo\\":\\"bar\\"}: Error: Excess keys are not allowed"` ); }); @@ -165,7 +165,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ searchFields }); await expect(find(findRequest, clientArgs, casesClientMock)).rejects.toThrow( - 'Error: Invalid value "foobar" supplied to "searchFields"' + 'Invalid option: expected one of' ); }); @@ -176,7 +176,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ searchFields }); await expect(find(findRequest, clientArgs, casesClientMock)).rejects.toThrow( - 'Error: Invalid value "foobar" supplied to "searchFields"' + 'Invalid option: expected one of' ); }); @@ -187,7 +187,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ sortField }); await expect(find(findRequest, clientArgs, casesClientMock)).rejects.toThrow( - 'Error: Invalid value "foobar" supplied to "sortField"' + 'Error: sortField: Invalid option: expected one of' ); }); @@ -197,7 +197,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ category }); await expect(find(findRequest, clientArgs, casesClientMock)).rejects.toThrow( - `Error: The length of the field category is too long. Array must be of length <= ${MAX_CATEGORY_FILTER_LENGTH}` + `Error: category: The length of the field category is too long. Array must be of length <= ${MAX_CATEGORY_FILTER_LENGTH}` ); }); @@ -207,7 +207,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ tags }); await expect(find(findRequest, clientArgs, casesClientMock)).rejects.toThrowError( - `Error: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_FILTER_LENGTH}` + `Error: tags: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_FILTER_LENGTH}` ); }); @@ -217,7 +217,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ assignees }); await expect(find(findRequest, clientArgs, casesClientMock)).rejects.toThrowError( - `Error: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_FILTER_LENGTH}` + `Error: assignees: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_FILTER_LENGTH}` ); }); @@ -227,7 +227,7 @@ describe('find', () => { const findRequest = createCasesClientMockFindRequest({ reporters }); await expect(find(findRequest, clientArgs, casesClientMock)).rejects.toThrowError( - `Error: The length of the field reporters is too long. Array must be of length <= ${MAX_REPORTERS_FILTER_LENGTH}.` + `Error: reporters: The length of the field reporters is too long. Array must be of length <= ${MAX_REPORTERS_FILTER_LENGTH}.` ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/find.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/find.ts index b3b18f0cd14a6..7a45b9bb9044e 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/find.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/find.ts @@ -13,8 +13,11 @@ import type { CasesFindRequestWithCustomFields, CasesFindResponse, } from '../../../common/types/api'; -import { CasesFindRequestWithCustomFieldsRt, CasesFindResponseRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { + CasesFindRequestWithCustomFieldsSchema, + CasesFindResponseSchema, +} from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import { asArray, transformCases } from '../../common/utils'; @@ -44,7 +47,9 @@ export const find = async ( } = clientArgs; try { - const paramArgs = decodeWithExcessOrThrow(CasesFindRequestWithCustomFieldsRt)(params); + const paramArgs = decodeWithExcessOrThrowZod(CasesFindRequestWithCustomFieldsSchema)( + params + ) as CasesFindRequestWithCustomFields; const configArgs = paramArgs.owner ? { owner: paramArgs.owner } : {}; const configurations = await casesClient.configure.get(configArgs); const customFieldsConfiguration: CustomFieldsConfiguration = configurations @@ -147,7 +152,7 @@ export const find = async ( countClosedCases: statusStats.closed, }); - return decodeOrThrow(CasesFindResponseRt)(res); + return decodeOrThrowZod(CasesFindResponseSchema)(res) as CasesFindResponse; } catch (error) { throw createCaseError({ message: `Failed to find cases: ${JSON.stringify(params)}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/get.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/get.test.ts index ada2703f52753..3397406b8bdf0 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/get.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/get.test.ts @@ -23,7 +23,7 @@ describe('get', () => { { options: { owner: 'cases', foo: 'bar' }, alertID: 'test-alert' }, clientArgs ) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); }); @@ -31,7 +31,7 @@ describe('get', () => { it('throws with excess fields', async () => { // @ts-expect-error: excess attribute await expect(getTags({ owner: 'cases', foo: 'bar' }, clientArgs)).rejects.toThrow( - 'invalid keys "foo"' + 'Excess keys are not allowed' ); }); }); @@ -40,7 +40,7 @@ describe('get', () => { it('throws with excess fields', async () => { // @ts-expect-error: excess attribute await expect(getReporters({ owner: 'cases', foo: 'bar' }, clientArgs)).rejects.toThrow( - 'invalid keys "foo"' + 'Excess keys are not allowed' ); }); }); @@ -49,7 +49,7 @@ describe('get', () => { it('throws with excess fields', async () => { // @ts-expect-error: excess attribute await expect(getCategories({ owner: 'cases', foo: 'bar' }, clientArgs)).rejects.toThrow( - 'invalid keys "foo"' + 'Excess keys are not allowed' ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts index 3466df8ef89c9..1cb909dd05d96 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/get.ts @@ -26,17 +26,17 @@ import type { GetRelatedCasesByAlertResponse, } from '../../../common/types/api'; import { - AllCategoriesFindRequestRt, - AllReportersFindRequestRt, - AllTagsFindRequestRt, - CaseResolveResponseRt, - CasesByAlertIDRequestRt, - GetCategoriesResponseRt, - GetRelatedCasesByAlertResponseRt, - GetReportersResponseRt, - GetTagsResponseRt, + AllCategoriesFindRequestSchema, + AllReportersFindRequestSchema, + AllTagsFindRequestSchema, + CaseResolveResponseSchema, + CasesByAlertIDRequestSchema, + GetCategoriesResponseSchema, + GetRelatedCasesByAlertResponseSchema, + GetReportersResponseSchema, + GetTagsResponseSchema, } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import { countAlertsForID, @@ -52,7 +52,7 @@ import type { CaseSavedObjectTransformed, CaseTransformedAttributes, } from '../../common/types/case'; -import { CaseRt } from '../../../common/types/domain'; +import { CaseSchema } from '../../../common/types/domain'; import type { AttachmentMode } from '../../../common/types/domain/attachment/v2'; /** @@ -86,7 +86,7 @@ export const getCasesByAlertID = async ( } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(CasesByAlertIDRequestRt)(options); + const queryParams = decodeWithExcessOrThrowZod(CasesByAlertIDRequestSchema)(options); const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = await authorization.getAuthorizationFilter(Operations.getCaseIDsByAlertID); @@ -149,7 +149,9 @@ export const getCasesByAlertID = async ( totals: getAttachmentTotalsForCaseId(caseInfo.id, commentStats), })); - return decodeOrThrow(GetRelatedCasesByAlertResponseRt)(res); + return decodeOrThrowZod(GetRelatedCasesByAlertResponseSchema)( + res + ) as GetRelatedCasesByAlertResponse; } catch (error) { throw createCaseError({ message: `Failed to get case IDs using alert ID: ${alertID} options: ${JSON.stringify( @@ -221,7 +223,7 @@ export const get = async ( const commentStats = await attachmentService.getter.getCaseAttatchmentStats({ caseIds: [theCase.id], }); - return decodeOrThrow(CaseRt)( + return decodeOrThrowZod(CaseSchema)( flattenCaseSavedObject({ savedObject: theCase, ...(commentStats.has(theCase.id) @@ -232,7 +234,7 @@ export const get = async ( } : {}), }) - ); + ) as Case; } const theComments = (await caseService.getAllCaseComments({ @@ -252,7 +254,7 @@ export const get = async ( totalEvents: countEventsForID({ comments: theComments }), }); - return decodeOrThrow(CaseRt)(res); + return decodeOrThrowZod(CaseSchema)(res) as Case; } catch (error) { throw createCaseError({ message: `Failed to get case id: ${id}: ${error}`, error, logger }); } @@ -292,12 +294,12 @@ export const resolve = async ( }); if (!includeComments) { - return decodeOrThrow(CaseResolveResponseRt)({ + return decodeOrThrowZod(CaseResolveResponseSchema)({ ...resolveData, case: flattenCaseSavedObject({ savedObject: resolvedSavedObject, }), - }); + }) as CaseResolveResponse; } const theComments = (await caseService.getAllCaseComments({ @@ -320,7 +322,7 @@ export const resolve = async ( }), }; - return decodeOrThrow(CaseResolveResponseRt)(res); + return decodeOrThrowZod(CaseResolveResponseSchema)(res) as CaseResolveResponse; } catch (error) { throw createCaseError({ message: `Failed to resolve case id: ${id}: ${error}`, error, logger }); } @@ -342,7 +344,7 @@ export async function getTags( } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(AllTagsFindRequestRt)(params); + const queryParams = decodeWithExcessOrThrowZod(AllTagsFindRequestSchema)(params); const { filter: authorizationFilter } = await authorization.getAuthorizationFilter( Operations.getTags @@ -355,7 +357,7 @@ export async function getTags( filter, }); - return decodeOrThrow(GetTagsResponseRt)(tags); + return decodeOrThrowZod(GetTagsResponseSchema)(tags); } catch (error) { throw createCaseError({ message: `Failed to get tags: ${error}`, error, logger }); } @@ -376,7 +378,7 @@ export async function getReporters( } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(AllReportersFindRequestRt)(params); + const queryParams = decodeWithExcessOrThrowZod(AllReportersFindRequestSchema)(params); const { filter: authorizationFilter } = await authorization.getAuthorizationFilter( Operations.getReporters @@ -389,7 +391,7 @@ export async function getReporters( filter, }); - return decodeOrThrow(GetReportersResponseRt)(reporters); + return decodeOrThrowZod(GetReportersResponseSchema)(reporters) as User[]; } catch (error) { throw createCaseError({ message: `Failed to get reporters: ${error}`, error, logger }); } @@ -410,7 +412,7 @@ export async function getCategories( } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(AllCategoriesFindRequestRt)(params); + const queryParams = decodeWithExcessOrThrowZod(AllCategoriesFindRequestSchema)(params); const { filter: authorizationFilter } = await authorization.getAuthorizationFilter( Operations.getCategories @@ -423,7 +425,7 @@ export async function getCategories( filter, }); - return decodeOrThrow(GetCategoriesResponseRt)(categories); + return decodeOrThrowZod(GetCategoriesResponseSchema)(categories); } catch (error) { throw createCaseError({ message: `Failed to get categories: ${error}`, error, logger }); } diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts index 5363d8a3ab1d7..98ca990845f4a 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/observables.ts @@ -10,20 +10,22 @@ import { v4 } from 'uuid'; import Boom from '@hapi/boom'; import { MAX_OBSERVABLES_PER_CASE } from '../../../common/constants'; -import type { Observable } from '../../../common/types/domain'; -import { CaseRt, UserActionTypes } from '../../../common/types/domain'; +import type { Case, Observable } from '../../../common/types/domain'; +import { UserActionTypes, CaseSchema } from '../../../common/types/domain'; import { - AddObservableRequestRt, type AddObservableRequest, type UpdateObservableRequest, - UpdateObservableRequestRt, type BulkAddObservablesRequest, - BulkAddObservablesRequestRt, type ObservablePost, } from '../../../common/types/api'; +import { + AddObservableRequestSchema, + UpdateObservableRequestSchema, + BulkAddObservablesRequestSchema, +} from '../../../common/types/api'; import type { CasesClient } from '../client'; import type { CasesClientArgs } from '../types'; -import { decodeOrThrow, decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod, decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import type { Authorization } from '../../authorization'; import { Operations } from '../../authorization'; import type { CaseSavedObjectTransformed } from '../../common/types/case'; @@ -74,7 +76,9 @@ export const addObservable = async ( licensingService.notifyUsage(LICENSING_CASE_OBSERVABLES_FEATURE); try { - const paramArgs = decodeWithExcessOrThrow(AddObservableRequestRt)(params); + const paramArgs = decodeWithExcessOrThrowZod(AddObservableRequestSchema)( + params + ) as AddObservableRequest; const retrievedCase = await caseService.getCase({ id: caseId }); await ensureUpdateAuthorized(authorization, retrievedCase); @@ -135,7 +139,7 @@ export const addObservable = async ( }, }); - return decodeOrThrow(CaseRt)(res); + return decodeOrThrowZod(CaseSchema)(res) as Case; } catch (error) { throw Boom.badRequest(`Failed to add observable: ${error}`); } @@ -165,7 +169,9 @@ export const updateObservable = async ( licensingService.notifyUsage(LICENSING_CASE_OBSERVABLES_FEATURE); try { - const paramArgs = decodeWithExcessOrThrow(UpdateObservableRequestRt)(params); + const paramArgs = decodeWithExcessOrThrowZod(UpdateObservableRequestSchema)( + params + ) as UpdateObservableRequest; const retrievedCase = await caseService.getCase({ id: caseId }); await ensureUpdateAuthorized(authorization, retrievedCase); @@ -225,7 +231,7 @@ export const updateObservable = async ( }, }); - return decodeOrThrow(CaseRt)(res); + return decodeOrThrowZod(CaseSchema)(res) as Case; } catch (error) { throw Boom.badRequest(`Failed to update observable: ${error}`); } @@ -312,7 +318,9 @@ export const bulkAddObservables = async ( licensingService.notifyUsage(LICENSING_CASE_OBSERVABLES_FEATURE); try { - const paramArgs = decodeWithExcessOrThrow(BulkAddObservablesRequestRt)(params); + const paramArgs = decodeWithExcessOrThrowZod(BulkAddObservablesRequestSchema)( + params + ) as BulkAddObservablesRequest; const retrievedCase = await caseService.getCase({ id: paramArgs.caseId }); await ensureUpdateAuthorized(authorization, retrievedCase); @@ -372,7 +380,7 @@ export const bulkAddObservables = async ( }, }); - return decodeOrThrow(CaseRt)(res); + return decodeOrThrowZod(CaseSchema)(res) as Case; } catch (error) { throw Boom.badRequest(`Failed to add observable: ${error}`); } diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/push.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/push.ts index 8f972c1bf744a..490e6915cbb78 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/push.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/push.ts @@ -20,10 +20,10 @@ import type { ConfigurationAttributes, } from '../../../common/types/domain'; import { - CaseRt, CaseStatuses, UserActionTypes, AttachmentType, + CaseSchema, } from '../../../common/types/domain'; import { CASE_COMMENT_SAVED_OBJECT, @@ -48,7 +48,7 @@ import { Operations } from '../../authorization'; import { casesConnectors } from '../../connectors'; import { getAlerts } from '../alerts/get'; import { buildFilter } from '../utils'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import type { ExternalServiceResponse } from '../../../common/types/api'; /** @@ -327,7 +327,7 @@ export const push = async ( }), }); - return decodeOrThrow(CaseRt)(res); + return decodeOrThrowZod(CaseSchema)(res) as Case; } catch (error) { throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); } diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.test.ts index f4c3666db7083..80961ca94d07c 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.test.ts @@ -186,7 +186,7 @@ describe('Replace custom field', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"undefined\\" supplied to \\"value\\""` + `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid input: expected boolean, received undefined, Invalid input: expected null, received undefined, Invalid input: expected string, received undefined, Invalid input: expected number, received undefined"` ); }); @@ -243,7 +243,7 @@ describe('Replace custom field', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\" \\" supplied to \\"value\\",The value field cannot be an empty string."` + `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: value: The value field cannot be an empty string."` ); }); @@ -263,7 +263,7 @@ describe('Replace custom field', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"undefined\\" supplied to \\"value\\""` + `"Failed to replace customField, id: first_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid input: expected boolean, received undefined, Invalid input: expected null, received undefined, Invalid input: expected string, received undefined, Invalid input: expected number, received undefined"` ); }); @@ -303,7 +303,7 @@ describe('Replace custom field', () => { casesClient ) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to replace customField, id: second_key of case: mock-id-1 version:WzAsMV0= : Error: Invalid value \\"foobar\\" supplied to \\"value\\""` + `"Failed to replace customField, id: second_key of case: mock-id-1 version:WzAsMV0= : Error: type: Invalid input: expected \\"text\\", value: Invalid input: expected boolean, received string, type: Invalid input: expected \\"number\\", Invalid input: expected number, received string, Invalid input: expected null, received string"` ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.ts index c57acee4b0a6c..9b14819da7210 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/replace_custom_field.ts @@ -8,13 +8,16 @@ import Boom from '@hapi/boom'; import type { CasesClient, CasesClientArgs } from '..'; -import type { CustomFieldPutRequest } from '../../../common/types/api'; -import { CustomFieldPutRequestRt, CaseRequestCustomFieldsRt } from '../../../common/types/api'; +import type { CustomFieldPutRequest, CaseRequestCustomFields } from '../../../common/types/api'; +import { + CustomFieldPutRequestSchema, + CaseRequestCustomFieldsSchema, +} from '../../../common/types/api'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import type { CaseCustomField } from '../../../common/types/domain'; -import { CaseCustomFieldRt } from '../../../common/types/domain'; +import { CaseCustomFieldSchema } from '../../../common/types/domain'; import { validateCustomFieldTypesInRequest } from './validators'; import type { UserActionEvent } from '../../services/user_actions/types'; import { validateMaxUserActions } from '../../common/validators'; @@ -54,7 +57,7 @@ export const replaceCustomField = async ( try { const { value, caseVersion } = request; - decodeWithExcessOrThrow(CustomFieldPutRequestRt)(request); + decodeWithExcessOrThrowZod(CustomFieldPutRequestSchema)(request); const caseToUpdate = await caseService.getCase({ id: caseId, @@ -107,8 +110,9 @@ export const replaceCustomField = async ( ...caseToUpdate.attributes.customFields.filter((field) => field.key !== customFieldId), ]; - const decodedCustomFields = - decodeWithExcessOrThrow(CaseRequestCustomFieldsRt)(customFieldsToUpdate); + const decodedCustomFields = decodeWithExcessOrThrowZod(CaseRequestCustomFieldsSchema)( + customFieldsToUpdate + ) as CaseRequestCustomFields; const updatedAt = new Date().toISOString(); @@ -156,7 +160,7 @@ export const replaceCustomField = async ( builtUserActions, }); - return decodeOrThrow(CaseCustomFieldRt)(updatedCustomField); + return decodeOrThrowZod(CaseCustomFieldSchema)(updatedCustomField) as CaseCustomField; } catch (error) { throw createCaseError({ message: `Failed to replace customField, id: ${customFieldId} of case: ${caseId} version:${request.caseVersion} : ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/search.test.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/search.test.ts index f7ee92bf8113c..b140adbd3578d 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/search.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/search.test.ts @@ -160,7 +160,7 @@ describe('search', () => { // @ts-expect-error foo is an invalid field search({ ...searchRequest, foo: 'bar' }, clientArgs, casesClientMock) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Failed to find cases: {\\"search\\":\\"sample_text\\",\\"searchFields\\":[\\"cases.title\\",\\"cases.description\\",\\"cases.incremental_id.text\\",\\"cases-comments.comment\\"],\\"severity\\":\\"low\\",\\"assignees\\":[],\\"reporters\\":[],\\"status\\":\\"open\\",\\"tags\\":[],\\"owner\\":[],\\"sortField\\":\\"createdAt\\",\\"sortOrder\\":\\"desc\\",\\"customFields\\":{},\\"foo\\":\\"bar\\"}: Error: invalid keys \\"foo\\""` + `"Failed to find cases: {\\"search\\":\\"sample_text\\",\\"searchFields\\":[\\"cases.title\\",\\"cases.description\\",\\"cases.incremental_id.text\\",\\"cases-comments.comment\\"],\\"severity\\":\\"low\\",\\"assignees\\":[],\\"reporters\\":[],\\"status\\":\\"open\\",\\"tags\\":[],\\"owner\\":[],\\"sortField\\":\\"createdAt\\",\\"sortOrder\\":\\"desc\\",\\"customFields\\":{},\\"foo\\":\\"bar\\"}: Error: Excess keys are not allowed"` ); }); @@ -171,7 +171,7 @@ describe('search', () => { const searchRequest = createCasesClientMockSearchRequest({ searchFields }); await expect(search(searchRequest, clientArgs, casesClientMock)).rejects.toThrow( - 'Error: Invalid value "foobar" supplied to "searchFields"' + 'Invalid option: expected one of' ); }); @@ -182,7 +182,7 @@ describe('search', () => { const searchRequest = createCasesClientMockSearchRequest({ searchFields }); await expect(search(searchRequest, clientArgs, casesClientMock)).rejects.toThrow( - 'Error: Invalid value "foobar" supplied to "searchFields"' + 'Invalid option: expected one of' ); }); @@ -193,7 +193,7 @@ describe('search', () => { const searchRequest = createCasesClientMockSearchRequest({ sortField }); await expect(search(searchRequest, clientArgs, casesClientMock)).rejects.toThrow( - 'Error: Invalid value "foobar" supplied to "sortField"' + 'Error: sortField: Invalid option: expected one of' ); }); @@ -203,7 +203,7 @@ describe('search', () => { const searchRequest = createCasesClientMockSearchRequest({ category }); await expect(search(searchRequest, clientArgs, casesClientMock)).rejects.toThrow( - `Error: The length of the field category is too long. Array must be of length <= ${MAX_CATEGORY_FILTER_LENGTH}` + `Error: category: The length of the field category is too long. Array must be of length <= ${MAX_CATEGORY_FILTER_LENGTH}` ); }); @@ -213,7 +213,7 @@ describe('search', () => { const searchRequest = createCasesClientMockSearchRequest({ tags }); await expect(search(searchRequest, clientArgs, casesClientMock)).rejects.toThrowError( - `Error: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_FILTER_LENGTH}` + `Error: tags: The length of the field tags is too long. Array must be of length <= ${MAX_TAGS_FILTER_LENGTH}` ); }); @@ -223,7 +223,7 @@ describe('search', () => { const searchRequest = createCasesClientMockSearchRequest({ assignees }); await expect(search(searchRequest, clientArgs, casesClientMock)).rejects.toThrowError( - `Error: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_FILTER_LENGTH}` + `Error: assignees: The length of the field assignees is too long. Array must be of length <= ${MAX_ASSIGNEES_FILTER_LENGTH}` ); }); @@ -233,7 +233,7 @@ describe('search', () => { const searchRequest = createCasesClientMockSearchRequest({ reporters }); await expect(search(searchRequest, clientArgs, casesClientMock)).rejects.toThrowError( - `Error: The length of the field reporters is too long. Array must be of length <= ${MAX_REPORTERS_FILTER_LENGTH}.` + `Error: reporters: The length of the field reporters is too long. Array must be of length <= ${MAX_REPORTERS_FILTER_LENGTH}.` ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/search.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/search.ts index 28ab827d9193b..6f6eadd927ced 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/search.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/search.ts @@ -12,8 +12,8 @@ import { spaceIdToNamespace } from '@kbn/spaces-plugin/server/lib/utils/namespac import { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server'; import type { CustomFieldsConfiguration } from '../../../common/types/domain'; import type { CasesSearchRequest, CasesFindResponse } from '../../../common/types/api'; -import { CasesSearchRequestRt, CasesFindResponseRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { CasesSearchRequestSchema, CasesFindResponseSchema } from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import { asArray, transformCases } from '../../common/utils'; @@ -49,7 +49,9 @@ export const search = async ( } = clientArgs; try { - const paramArgs = decodeWithExcessOrThrow(CasesSearchRequestRt)(params); + const paramArgs = decodeWithExcessOrThrowZod(CasesSearchRequestSchema)( + params + ) as CasesSearchRequest; const configArgs = paramArgs.owner ? { owner: paramArgs.owner } : {}; const configurations = await casesClient.configure.get(configArgs); const customFieldsConfiguration: CustomFieldsConfiguration = configurations @@ -190,7 +192,7 @@ export const search = async ( res.cases = enrichCasesWithFieldLabels(res.cases, templateSOs); - return decodeOrThrow(CasesFindResponseRt)(res); + return decodeOrThrowZod(CasesFindResponseSchema)(res) as CasesFindResponse; } catch (error) { throw createCaseError({ message: `Failed to find cases: ${JSON.stringify(params)}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/cases/similar.ts b/x-pack/platform/plugins/shared/cases/server/client/cases/similar.ts index 08911cd6bf3df..db73e8f8eb801 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/cases/similar.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/cases/similar.ts @@ -10,8 +10,11 @@ import Boom from '@hapi/boom'; import type { ObservableType } from '../../../common/types/domain/observable/v1'; import { OWNER_FIELD } from '../../../common/constants'; import type { CasesSimilarResponse, SimilarCasesSearchRequest } from '../../../common/types/api'; -import { SimilarCasesSearchRequestRt, CasesSimilarResponseRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { + SimilarCasesSearchRequestSchema, + CasesSimilarResponseSchema, +} from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import type { CasesClient, CasesClientArgs } from '..'; @@ -77,7 +80,9 @@ export const similar = async ( } try { - const paramArgs = decodeWithExcessOrThrow(SimilarCasesSearchRequestRt)(params); + const paramArgs = decodeWithExcessOrThrowZod(SimilarCasesSearchRequestSchema)( + params + ) as SimilarCasesSearchRequest; const retrievedCase = await caseService.getCase({ id: caseId }); const availableObservableTypesMap = await getAvailableObservableTypesMap( @@ -154,7 +159,7 @@ export const similar = async ( total: cases.total, }; - return decodeOrThrow(CasesSimilarResponseRt)(res); + return decodeOrThrowZod(CasesSimilarResponseSchema)(res) as CasesSimilarResponse; } catch (error) { throw createCaseError({ message: `Failed to find cases: ${JSON.stringify(params)}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/configure/client.test.ts b/x-pack/platform/plugins/shared/cases/server/client/configure/client.test.ts index 4ad97b0f335b0..43008dd3690e2 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/configure/client.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/configure/client.test.ts @@ -234,7 +234,7 @@ describe('client', () => { await expect( // @ts-expect-error: excess attribute get({ owner: 'cases', foo: 'bar' }, clientArgs, casesClientInternal) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); }); @@ -243,7 +243,7 @@ describe('client', () => { await expect( // @ts-expect-error: excess attribute update('test-id', { version: 'test-version', foo: 'bar' }, clientArgs, casesClientInternal) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); it(`throws when trying to update more than ${MAX_CUSTOM_FIELDS_PER_CASE} custom fields`, async () => { @@ -263,7 +263,7 @@ describe('client', () => { casesClientInternal ) ).rejects.toThrow( - `Failed to get patch configure in route: Error: The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.` + `Failed to get patch configure in route: Error: customFields: The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.` ); }); @@ -506,7 +506,7 @@ describe('client', () => { casesClientInternal ) ).rejects.toThrow( - `Failed to get patch configure in route: Error: The length of the field templates is too long. Array must be of length <= ${MAX_TEMPLATES_LENGTH}.` + `Failed to get patch configure in route: Error: templates: The length of the field templates is too long. Array must be of length <= ${MAX_TEMPLATES_LENGTH}.` ); }); @@ -1242,7 +1242,7 @@ describe('client', () => { casesClientInternal ) ).rejects.toThrow( - `Failed to create case configuration: Error: The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.` + `Failed to create case configuration: Error: customFields: The length of the field customFields is too long. Array must be of length <= ${MAX_CUSTOM_FIELDS_PER_CASE}.` ); }); @@ -1291,7 +1291,7 @@ describe('client', () => { casesClientInternal ) ).rejects.toThrow( - `Failed to create case configuration: Error: The length of the field templates is too long. Array must be of length <= ${MAX_TEMPLATES_LENGTH}.` + `Failed to create case configuration: Error: templates: The length of the field templates is too long. Array must be of length <= ${MAX_TEMPLATES_LENGTH}.` ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/configure/client.ts b/x-pack/platform/plugins/shared/cases/server/client/configure/client.ts index 4b4800f3c8657..be05dcb566287 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/configure/client.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/configure/client.ts @@ -28,12 +28,12 @@ import type { GetConfigurationFindRequest, } from '../../../common/types/api'; import { - ConfigurationPatchRequestRt, - ConfigurationRequestRt, - GetConfigurationFindRequestRt, - FindActionConnectorResponseRt, + ConfigurationPatchRequestSchema, + ConfigurationRequestSchema, + GetConfigurationFindRequestSchema, + FindActionConnectorResponseSchema, } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { MAX_CONCURRENT_SEARCHES, MAX_SUPPORTED_CONNECTORS_RETURNED, @@ -48,7 +48,7 @@ import { combineAuthorizedAndOwnerFilter, transformTemplateCustomFields } from ' import type { MappingsArgs, CreateMappingsArgs, UpdateMappingsArgs } from './types'; import { createMappings } from './create_mappings'; import { updateMappings } from './update_mappings'; -import { ConfigurationRt, ConfigurationsRt } from '../../../common/types/domain'; +import { ConfigurationSchema, ConfigurationsSchema } from '../../../common/types/domain'; import { validateDuplicatedKeysInRequest, validateDuplicatedObservableTypesInRequest, @@ -196,7 +196,7 @@ export async function get( } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(GetConfigurationFindRequestRt)(params); + const queryParams = decodeWithExcessOrThrowZod(GetConfigurationFindRequestSchema)(params); const { filter: authorizationFilter, ensureSavedObjectsAreAuthorized } = await authorization.getAuthorizationFilter(Operations.findConfigurations); @@ -252,7 +252,7 @@ export async function get( } ); - return decodeOrThrow(ConfigurationsRt)(configurations); + return decodeOrThrowZod(ConfigurationsSchema)(configurations) as Configurations; } catch (error) { throw createCaseError({ message: `Failed to get case configure: ${error}`, error, logger }); } @@ -272,7 +272,7 @@ export async function getConnectors({ .filter((action) => isConnectorSupported(action, actionTypes)) .slice(0, MAX_SUPPORTED_CONNECTORS_RETURNED); - return decodeOrThrow(FindActionConnectorResponseRt)(res); + return decodeOrThrowZod(FindActionConnectorResponseSchema)(res); } catch (error) { throw createCaseError({ message: `Failed to get connectors: ${error}`, error, logger }); } @@ -304,7 +304,9 @@ export async function update( } = clientArgs; try { - const request = decodeWithExcessOrThrow(ConfigurationPatchRequestRt)(req); + const request = decodeWithExcessOrThrowZod(ConfigurationPatchRequestSchema)( + req + ) as ConfigurationPatchRequest; validateDuplicatedKeysInRequest({ requestFields: request.customFields, @@ -411,7 +413,7 @@ export async function update( id: patch.id, }; - return decodeOrThrow(ConfigurationRt)(res); + return decodeOrThrowZod(ConfigurationSchema)(res) as Configuration; } catch (error) { throw createCaseError({ message: `Failed to get patch configure in route: ${error}`, @@ -435,8 +437,9 @@ export async function create( } = clientArgs; try { - const validatedConfigurationRequest = - decodeWithExcessOrThrow(ConfigurationRequestRt)(configRequest); + const validatedConfigurationRequest = decodeWithExcessOrThrowZod(ConfigurationRequestSchema)( + configRequest + ) as ConfigurationRequest; validateDuplicatedKeysInRequest({ requestFields: validatedConfigurationRequest.customFields, @@ -547,7 +550,7 @@ export async function create( id: post.id, }; - return decodeOrThrow(ConfigurationRt)(res); + return decodeOrThrowZod(ConfigurationSchema)(res) as Configuration; } catch (error) { throw createCaseError({ message: `Failed to create case configuration: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/configure/create_mappings.ts b/x-pack/platform/plugins/shared/cases/server/client/configure/create_mappings.ts index 2116947fd4e60..993f77b62f33b 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/configure/create_mappings.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/configure/create_mappings.ts @@ -7,8 +7,8 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import type { ConnectorMappingResponse } from '../../../common/types/api'; -import { ConnectorMappingResponseRt } from '../../../common/types/api'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { ConnectorMappingResponseSchema } from '../../../common/types/api'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import type { CasesClientArgs } from '..'; import type { CreateMappingsArgs } from './types'; @@ -49,7 +49,7 @@ export const createMappings = async ( mappings: theMapping.attributes.mappings, }; - return decodeOrThrow(ConnectorMappingResponseRt)(res); + return decodeOrThrowZod(ConnectorMappingResponseSchema)(res); } catch (error) { throw createCaseError({ message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/configure/get_mappings.ts b/x-pack/platform/plugins/shared/cases/server/client/configure/get_mappings.ts index c1becb1600668..a086d3f499ef8 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/configure/get_mappings.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/configure/get_mappings.ts @@ -7,8 +7,8 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import type { ConnectorMappingResponse } from '../../../common/types/api'; -import { ConnectorMappingResponseRt } from '../../../common/types/api'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { ConnectorMappingResponseSchema } from '../../../common/types/api'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import type { CasesClientArgs } from '..'; import type { MappingsArgs } from './types'; @@ -46,7 +46,7 @@ export const getMappings = async ( mappings: so.attributes.mappings, }; - return decodeOrThrow(ConnectorMappingResponseRt)(res); + return decodeOrThrowZod(ConnectorMappingResponseSchema)(res); } catch (error) { throw createCaseError({ message: `Failed to retrieve mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/configure/update_mappings.ts b/x-pack/platform/plugins/shared/cases/server/client/configure/update_mappings.ts index 2952fa3238b95..1c4832c2c3ad2 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/configure/update_mappings.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/configure/update_mappings.ts @@ -7,8 +7,8 @@ import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import type { ConnectorMappingResponse } from '../../../common/types/api'; -import { ConnectorMappingResponseRt } from '../../../common/types/api'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { ConnectorMappingResponseSchema } from '../../../common/types/api'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import type { CasesClientArgs } from '..'; import type { UpdateMappingsArgs } from './types'; @@ -49,7 +49,7 @@ export const updateMappings = async ( mappings: theMapping.attributes.mappings, }; - return decodeOrThrow(ConnectorMappingResponseRt)(res); + return decodeOrThrowZod(ConnectorMappingResponseSchema)(res); } catch (error) { throw createCaseError({ message: `Failed to create mapping connector id: ${connector.id} type: ${connector.type}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.test.ts b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.test.ts index 3702ff3336410..1872c02c94b2e 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.test.ts @@ -145,7 +145,7 @@ describe('getCaseMetrics', () => { ); } catch (error) { expect(error.message).toMatchInlineSnapshot( - `"Failed to retrieve metrics within client for case id: 1: Error: Invalid value \\"bananas\\" supplied to \\"features\\""` + `"Failed to retrieve metrics within client for case id: 1: Error: Invalid input: expected \\"alerts.count\\", Invalid input: expected \\"alerts.users\\", Invalid input: expected \\"alerts.hosts\\", Invalid input: expected \\"actions.isolateHost\\", Invalid input: expected \\"connectors\\", and 1 more"` ); } }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.ts b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.ts index c8e8ca72d1f28..7d2af79360825 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_case_metrics.ts @@ -7,8 +7,11 @@ import { merge } from 'lodash'; import type { SingleCaseMetricsResponse } from '../../../common/types/api'; -import { SingleCaseMetricsResponseRt, SingleCaseMetricsRequestRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { + SingleCaseMetricsResponseSchema, + SingleCaseMetricsRequestSchema, +} from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; import type { CasesClient } from '../client'; @@ -24,7 +27,7 @@ export const getCaseMetrics = async ( const { logger } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(SingleCaseMetricsRequestRt)({ features }); + const queryParams = decodeWithExcessOrThrowZod(SingleCaseMetricsRequestSchema)({ features }); await checkAuthorization(caseId, clientArgs); const handlers = buildHandlers( @@ -43,7 +46,9 @@ export const getCaseMetrics = async ( return merge(acc, metric); }, {}) as SingleCaseMetricsResponse; - return decodeOrThrow(SingleCaseMetricsResponseRt)(mergedResults); + return decodeOrThrowZod(SingleCaseMetricsResponseSchema)( + mergedResults + ) as SingleCaseMetricsResponse; } catch (error) { throw createCaseError({ logger, diff --git a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.test.ts b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.test.ts index 867129ecab984..6c420b4094e6a 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.test.ts @@ -37,7 +37,7 @@ describe('getCasesMetrics', () => { await expect( // @ts-expect-error: excess attribute getCasesMetrics({ features: [CaseMetricsFeature.MTTR], foo: 'bar' }, client, clientArgs) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); it('returns the mttr metric', async () => { @@ -145,7 +145,7 @@ describe('getCasesMetrics', () => { it('throws with unknown feature value', async () => { // @ts-expect-error: invalid feature value await expect(getCasesMetrics({ features: ['foobar'] }, client, clientArgs)).rejects.toThrow( - 'Invalid value "foobar" supplied to "features"' + 'Invalid input: expected' ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.ts b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.ts index 7577ac770bab5..7f7aa4538c999 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_cases_metrics.ts @@ -8,8 +8,8 @@ import { merge } from 'lodash'; import type { CasesMetricsRequest, CasesMetricsResponse } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; -import { CasesMetricsRequestRt, CasesMetricsResponseRt } from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; +import { CasesMetricsRequestSchema, CasesMetricsResponseSchema } from '../../../common/types/api'; import { createCaseError } from '../../common/error'; import type { CasesClient } from '../client'; import type { CasesClientArgs } from '../types'; @@ -23,7 +23,9 @@ export const getCasesMetrics = async ( const { logger } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(CasesMetricsRequestRt)(params); + const queryParams = decodeWithExcessOrThrowZod(CasesMetricsRequestSchema)( + params + ) as CasesMetricsRequest; const handlers = buildHandlers(queryParams, casesClient, clientArgs); @@ -37,7 +39,7 @@ export const getCasesMetrics = async ( return merge(acc, metric); }, {}) as CasesMetricsResponse; - return decodeOrThrow(CasesMetricsResponseRt)(mergedResults); + return decodeOrThrowZod(CasesMetricsResponseSchema)(mergedResults) as CasesMetricsResponse; } catch (error) { throw createCaseError({ logger, diff --git a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.test.ts b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.test.ts index a3cce4413edde..9936b63d81243 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.test.ts @@ -35,7 +35,7 @@ describe('getStatusTotalsByType', () => { await expect( // @ts-expect-error: excess attribute getStatusTotalsByType({ foo: 'bar' }, clientArgs) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); it('returns the status correctly', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.ts b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.ts index b7ca0e91debeb..a2e19b0225fc6 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/metrics/get_status_totals.ts @@ -6,8 +6,8 @@ */ import type { CasesStatusRequest, CasesStatusResponse } from '../../../common/types/api'; -import { CasesStatusRequestRt, CasesStatusResponseRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import { CasesStatusRequestSchema, CasesStatusResponseSchema } from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import type { CasesClientArgs } from '../types'; import { Operations } from '../../authorization'; import { constructQueryOptions } from '../utils'; @@ -24,7 +24,7 @@ export async function getStatusTotalsByType( } = clientArgs; try { - const queryParams = decodeWithExcessOrThrow(CasesStatusRequestRt)(params); + const queryParams = decodeWithExcessOrThrowZod(CasesStatusRequestSchema)(params); const { filter: authorizationFilter } = await authorization.getAuthorizationFilter( Operations.getCaseStatuses @@ -46,7 +46,7 @@ export async function getStatusTotalsByType( count_closed_cases: statusStats.closed, }; - return decodeOrThrow(CasesStatusResponseRt)(res); + return decodeOrThrowZod(CasesStatusResponseSchema)(res); } catch (error) { throw createCaseError({ message: `Failed to get status stats: ${error}`, error, logger }); } diff --git a/x-pack/platform/plugins/shared/cases/server/client/metrics/lifespan.ts b/x-pack/platform/plugins/shared/cases/server/client/metrics/lifespan.ts index b22a01ebd6fbb..4b1d808d5e57c 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/metrics/lifespan.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/metrics/lifespan.ts @@ -12,7 +12,7 @@ import type { SingleCaseMetricsResponse, StatusInfo, } from '../../../common/types/api'; -import { StatusUserActionRt, CaseStatuses } from '../../../common/types/domain'; +import { CaseStatuses, StatusUserActionSchema } from '../../../common/types/domain'; import { CaseMetricsFeature } from '../../../common/types/api'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; @@ -141,7 +141,9 @@ function isValidStatusChangeUserAction( attributes: UserActionAttributes, newStatusChangeTimestamp: Date ): attributes is UserActionWithResponse { - return StatusUserActionRt.is(attributes) && isDateValid(newStatusChangeTimestamp); + return ( + StatusUserActionSchema.safeParse(attributes).success && isDateValid(newStatusChangeTimestamp) + ); } function isReopen(newStatus: CaseStatuses, lastStatus: CaseStatuses): boolean { diff --git a/x-pack/platform/plugins/shared/cases/server/client/user_actions/connectors.ts b/x-pack/platform/plugins/shared/cases/server/client/user_actions/connectors.ts index 74fbd404c3cc3..6a2a1ef87fd23 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/user_actions/connectors.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/user_actions/connectors.ts @@ -14,8 +14,8 @@ import type { GetCaseConnectorsPushDetails, GetCaseConnectorsResponse, } from '../../../common/types/api'; -import { GetCaseConnectorsResponseRt } from '../../../common/types/api'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { GetCaseConnectorsResponseSchema } from '../../../common/types/api'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { isConnectorUserAction, isCreateCaseUserAction, @@ -63,7 +63,7 @@ export const getConnectors = async ( logger, }); - return decodeOrThrow(GetCaseConnectorsResponseRt)(res); + return decodeOrThrowZod(GetCaseConnectorsResponseSchema)(res) as GetCaseConnectorsResponse; } catch (error) { throw createCaseError({ message: `Failed to retrieve the case connectors case id: ${caseId}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.test.ts b/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.test.ts index e0655bc1d2d51..79b56ba7678ac 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.test.ts @@ -23,7 +23,7 @@ describe('findUserActions', () => { await expect( // @ts-expect-error: excess attribute find({ caseId: 'test-case', params: { foo: 'bar' } }, client, clientArgs) - ).rejects.toThrow('invalid keys "foo"'); + ).rejects.toThrow('Excess keys are not allowed'); }); it(`throws when trying to fetch more than ${MAX_DOCS_PER_PAGE} items`, async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.ts b/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.ts index e5a27c8b4f934..69ee4bcc7ad6b 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/user_actions/find.ts @@ -5,9 +5,12 @@ * 2.0. */ -import type { UserActionFindResponse } from '../../../common/types/api'; -import { UserActionFindRequestRt, UserActionFindResponseRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow, decodeOrThrow } from '../../common/runtime_types'; +import type { UserActionFindRequest, UserActionFindResponse } from '../../../common/types/api'; +import { + UserActionFindRequestSchema, + UserActionFindResponseSchema, +} from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod, decodeOrThrowZod } from '../../common/runtime_types'; import type { CasesClientArgs } from '../types'; import type { UserActionFind } from './types'; import { Operations } from '../../authorization'; @@ -31,7 +34,10 @@ export const find = async ( // supertest and query-string encode a single entry in an array as just a string so make sure we have an array const types = asArray(params.types); - const queryParams = decodeWithExcessOrThrow(UserActionFindRequestRt)({ ...params, types }); + const queryParams = decodeWithExcessOrThrowZod(UserActionFindRequestSchema)({ + ...params, + types, + }) as UserActionFindRequest; const [authorizationFilterRes] = await Promise.all([ authorization.getAuthorizationFilter(Operations.findUserActions), @@ -58,7 +64,7 @@ export const find = async ( total: userActions.total, }; - return decodeOrThrow(UserActionFindResponseRt)(res); + return decodeOrThrowZod(UserActionFindResponseSchema)(res) as UserActionFindResponse; } catch (error) { throw createCaseError({ message: `Failed to find user actions for case id: ${caseId}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/user_actions/get.ts b/x-pack/platform/plugins/shared/cases/server/client/user_actions/get.ts index 37fa29fdc4c31..ff2b2d11d7743 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/user_actions/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/user_actions/get.ts @@ -6,13 +6,13 @@ */ import type { CaseUserActionsDeprecatedResponse } from '../../../common/types/api'; -import { CaseUserActionsDeprecatedResponseRt } from '../../../common/types/api'; +import { CaseUserActionsDeprecatedResponseSchema } from '../../../common/types/api'; import { createCaseError } from '../../common/error'; import type { CasesClientArgs } from '..'; import { Operations } from '../../authorization'; import type { UserActionGet } from './types'; import { extractAttributes } from './utils'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod } from '../../common/runtime_types'; export const get = async ( { caseId }: UserActionGet, @@ -37,7 +37,9 @@ export const get = async ( const res = extractAttributes(userActions); - return decodeOrThrow(CaseUserActionsDeprecatedResponseRt)(res); + return decodeOrThrowZod(CaseUserActionsDeprecatedResponseSchema)( + res + ) as CaseUserActionsDeprecatedResponse; } catch (error) { throw createCaseError({ message: `Failed to retrieve user actions case id: ${caseId}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/user_actions/stats.ts b/x-pack/platform/plugins/shared/cases/server/client/user_actions/stats.ts index 26b3cd6d77fe9..68372f7ecdcb3 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/user_actions/stats.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/user_actions/stats.ts @@ -6,8 +6,8 @@ */ import type { CaseUserActionStatsResponse } from '../../../common/types/api'; -import { CaseUserActionStatsResponseRt } from '../../../common/types/api'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { CaseUserActionStatsResponseSchema } from '../../../common/types/api'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { createCaseError } from '../../common/error'; import type { CasesClientArgs } from '..'; import type { UserActionGet } from './types'; @@ -29,7 +29,7 @@ export const getStats = async ( caseId, }); - return decodeOrThrow(CaseUserActionStatsResponseRt)(totals); + return decodeOrThrowZod(CaseUserActionStatsResponseSchema)(totals); } catch (error) { throw createCaseError({ message: `Failed to retrieve user action stats for case id: ${caseId}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/user_actions/users.ts b/x-pack/platform/plugins/shared/cases/server/client/user_actions/users.ts index 834a0bb0edb6b..41aa082b219af 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/user_actions/users.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/user_actions/users.ts @@ -8,8 +8,8 @@ import { isEmpty, isString } from 'lodash'; import type { UserProfileAvatarData, UserProfileWithAvatar } from '@kbn/user-profile-components'; import type { GetCaseUsersResponse } from '../../../common/types/api'; -import { GetCaseUsersResponseRt } from '../../../common/types/api'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { GetCaseUsersResponseSchema } from '../../../common/types/api'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import type { OwnerEntity } from '../../authorization'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; @@ -106,7 +106,7 @@ export const getUsers = async ( reporter: reporterResponse[0], }; - return decodeOrThrow(GetCaseUsersResponseRt)(results); + return decodeOrThrowZod(GetCaseUsersResponseSchema)(results) as GetCaseUsersResponse; } catch (error) { throw createCaseError({ message: `Failed to retrieve the case users case id: ${caseId}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/client/utils.ts b/x-pack/platform/plugins/shared/cases/server/client/utils.ts index d671a1340142c..7e7c23446917b 100644 --- a/x-pack/platform/plugins/shared/cases/server/client/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/client/utils.ts @@ -30,19 +30,19 @@ import type { CustomFieldTypes, } from '../../common/types/domain'; import { - ActionsAttachmentPayloadRt, - AlertAttachmentPayloadRt, - EventAttachmentPayloadRt, - ExternalReferenceNoSOAttachmentPayloadRt, - ExternalReferenceSOAttachmentPayloadRt, ExternalReferenceStorageType, - PersistableStateAttachmentPayloadRt, - UserCommentAttachmentPayloadRt, + ActionsAttachmentPayloadSchema, + AlertAttachmentPayloadSchema, + EventAttachmentPayloadSchema, + ExternalReferenceNoSOAttachmentPayloadSchema, + ExternalReferenceSOAttachmentPayloadSchema, + PersistableStateAttachmentPayloadSchema, + UserCommentAttachmentPayloadSchema, } from '../../common/types/domain'; import type { SavedObjectFindOptionsKueryNode } from '../common/types'; import type { CasesSearchParams } from './types'; -import { decodeWithExcessOrThrow } from '../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../common/runtime_types'; import { CASE_SAVED_OBJECT, FILE_ATTACHMENT_TYPE, @@ -85,11 +85,11 @@ export const decodeCommentRequest = ( externalRefRegistry: ExternalReferenceAttachmentTypeRegistry ) => { if (isLegacyCommentAttachment(comment)) { - decodeWithExcessOrThrow(UserCommentAttachmentPayloadRt)(comment); + decodeWithExcessOrThrowZod(UserCommentAttachmentPayloadSchema)(comment); } else if (isCommentRequestTypeActions(comment)) { - decodeWithExcessOrThrow(ActionsAttachmentPayloadRt)(comment); + decodeWithExcessOrThrowZod(ActionsAttachmentPayloadSchema)(comment); } else if (isCommentRequestTypeAlert(comment)) { - decodeWithExcessOrThrow(AlertAttachmentPayloadRt)(comment); + decodeWithExcessOrThrowZod(AlertAttachmentPayloadSchema)(comment); const { ids, indices } = getIDsAndIndicesAsArrays(comment); @@ -134,11 +134,11 @@ export const decodeCommentRequest = ( ); } } else if (isCommentRequestTypeEvent(comment)) { - decodeWithExcessOrThrow(EventAttachmentPayloadRt)(comment); + decodeWithExcessOrThrowZod(EventAttachmentPayloadSchema)(comment); } else if (isCommentRequestTypeExternalReference(comment)) { decodeExternalReferenceAttachment(comment, externalRefRegistry); } else if (isCommentRequestTypePersistableState(comment)) { - decodeWithExcessOrThrow(PersistableStateAttachmentPayloadRt)(comment); + decodeWithExcessOrThrowZod(PersistableStateAttachmentPayloadSchema)(comment); } else { /** * This assertion ensures that TS will show an error @@ -154,9 +154,9 @@ const decodeExternalReferenceAttachment = ( externalRefRegistry: ExternalReferenceAttachmentTypeRegistry ) => { if (attachment.externalReferenceStorage.type === ExternalReferenceStorageType.savedObject) { - decodeWithExcessOrThrow(ExternalReferenceSOAttachmentPayloadRt)(attachment); + decodeWithExcessOrThrowZod(ExternalReferenceSOAttachmentPayloadSchema)(attachment); } else { - decodeWithExcessOrThrow(ExternalReferenceNoSOAttachmentPayloadRt)(attachment); + decodeWithExcessOrThrowZod(ExternalReferenceNoSOAttachmentPayloadSchema)(attachment); } const metadata = attachment.externalReferenceMetadata; diff --git a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts index 3ecdfdc56a4c5..f1e13641ba669 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/models/case_with_comments.ts @@ -26,11 +26,11 @@ import type { UserCommentAttachmentPayload, } from '../../../common/types/domain'; import { - CaseRt, CaseStatuses, UserActionActions, UserActionTypes, AttachmentType, + CaseSchema, } from '../../../common/types/domain'; import { CASE_SAVED_OBJECT, MAX_DOCS_PER_PAGE } from '../../../common/constants'; @@ -51,7 +51,7 @@ import { isCommentRequestTypeEvent, countEventsForID, } from '../utils'; -import { decodeOrThrow } from '../runtime_types'; +import { decodeOrThrowZod } from '../runtime_types'; import type { AttachmentRequest, AttachmentPatchRequestV2, @@ -581,7 +581,7 @@ export class CaseCommentModel { ...this.formatForEncoding(comments.total), }; - return decodeOrThrow(CaseRt)(caseResponse); + return decodeOrThrowZod(CaseSchema)(caseResponse) as Case; } catch (error) { throw createCaseError({ message: `Failed encoding the commentable case, case id: ${this.caseInfo.id}: ${error}`, diff --git a/x-pack/platform/plugins/shared/cases/server/common/runtime_types.test.ts b/x-pack/platform/plugins/shared/cases/server/common/runtime_types.test.ts index a10566e2dc8b2..42e3dfca11723 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/runtime_types.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/runtime_types.test.ts @@ -5,100 +5,100 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; -import { decodeWithExcessOrThrow } from './runtime_types'; +import { decodeOrThrowZod, decodeWithExcessOrThrowZod } from './runtime_types'; describe('runtime_types', () => { - describe('decodeWithExcessOrThrow', () => { - it('does not throw when all required fields are present for rt.type', () => { - const schemaRt = rt.type({ - a: rt.string, - }); - - expect(() => decodeWithExcessOrThrow(schemaRt)({ a: 'hi' })).not.toThrow(); + describe('decodeWithExcessOrThrowZod', () => { + it('does not throw when all required fields are present', () => { + const schema = z.object({ a: z.string() }); + expect(() => decodeWithExcessOrThrowZod(schema)({ a: 'hi' })).not.toThrow(); }); - it('does not throw when all required fields are present for rt.strict', () => { - const schemaRt = rt.strict({ - a: rt.string, - }); - - expect(() => decodeWithExcessOrThrow(schemaRt)({ a: 'hi' })).not.toThrow(); - }); - - it('throws when a required field is not present for rt.type', () => { - const schemaRt = rt.type({ - a: rt.string, - }); - - expect(() => decodeWithExcessOrThrow(schemaRt)({})).toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"a\\""` + it('throws Boom 400 when a required field is missing', () => { + const schema = z.object({ a: z.string() }); + expect(() => decodeWithExcessOrThrowZod(schema)({})).toThrow( + expect.objectContaining({ + isBoom: true, + output: expect.objectContaining({ statusCode: 400 }), + }) ); }); - it('throws when a required field is not present for rt.strict', () => { - const schemaRt = rt.strict({ - a: rt.string, - }); - - expect(() => decodeWithExcessOrThrow(schemaRt)({})).toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"a\\""` - ); + it('throws when an excess field exists at the top level', () => { + const schema = z.object({ a: z.string() }); + expect(() => + decodeWithExcessOrThrowZod(schema)({ a: 'hi', b: 1 }) + ).toThrowErrorMatchingInlineSnapshot(`"Excess keys are not allowed"`); }); - it('throws when an excess field exists for rt.strict', () => { - const schemaRt = rt.strict({ - a: rt.string, - }); - + it('throws when a nested excess field exists', () => { + const schema = z.object({ a: z.object({ b: z.string() }) }); expect(() => - decodeWithExcessOrThrow(schemaRt)({ a: 'hi', b: 1 }) - ).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"b\\""`); + decodeWithExcessOrThrowZod(schema)({ a: { b: 'hi', c: 1 } }) + ).toThrowErrorMatchingInlineSnapshot(`"Excess keys are not allowed"`); }); - it('does not throw when an excess field exists for rt.type', () => { - const schemaRt = rt.type({ - a: rt.string, - }); + it('returns the parsed object on success', () => { + const schema = z.object({ a: z.string() }); + expect(decodeWithExcessOrThrowZod(schema)({ a: 'hi' })).toStrictEqual({ a: 'hi' }); + }); - expect(() => decodeWithExcessOrThrow(schemaRt)({ a: 'hi', b: 1 })).not.toThrow(); + it('throws when an excess field exists inside an array of objects', () => { + const schema = z.object({ items: z.array(z.object({ a: z.string() })) }); + expect(() => + decodeWithExcessOrThrowZod(schema)({ items: [{ a: 'hi' }, { a: 'hi', b: 1 }] }) + ).toThrowErrorMatchingInlineSnapshot(`"Excess keys are not allowed"`); }); - it('throws when a nested excess field exists for rt.strict', () => { - const schemaRt = rt.strict({ - a: rt.strict({ - b: rt.string, - }), + it('throws when an excess field exists inside a union variant', () => { + const schema = z.object({ + payload: z.union([ + z.object({ kind: z.literal('a'), value: z.string() }), + z.object({ kind: z.literal('b') }), + ]), }); - expect(() => - decodeWithExcessOrThrow(schemaRt)({ a: { b: 'hi', c: 1 } }) - ).toThrowErrorMatchingInlineSnapshot(`"invalid keys \\"c\\""`); + decodeWithExcessOrThrowZod(schema)({ payload: { kind: 'a', value: 'hi', extra: 1 } }) + ).toThrowErrorMatchingInlineSnapshot(`"Excess keys are not allowed"`); }); - it('does not throw when a nested excess field exists for rt.type', () => { - const schemaRt = rt.type({ - a: rt.type({ b: rt.string }), + it('throws when an excess field exists inside a discriminated union variant', () => { + const schema = z.object({ + payload: z.discriminatedUnion('kind', [ + z.object({ kind: z.literal('a'), value: z.string() }), + z.object({ kind: z.literal('b') }), + ]), }); - - expect(() => decodeWithExcessOrThrow(schemaRt)({ a: { b: 'hi', c: 1 } })).not.toThrow(); + expect(() => + decodeWithExcessOrThrowZod(schema)({ payload: { kind: 'a', value: 'hi', extra: 1 } }) + ).toThrowErrorMatchingInlineSnapshot(`"Excess keys are not allowed"`); }); + }); - it('returns the object after decoding for rt.type', () => { - const schemaRt = rt.type({ - a: rt.string, - }); + describe('decodeOrThrowZod', () => { + it('returns the parsed value on success', () => { + const schema = z.object({ a: z.string() }); + expect(decodeOrThrowZod(schema)({ a: 'hi' })).toStrictEqual({ a: 'hi' }); + }); - expect(decodeWithExcessOrThrow(schemaRt)({ a: 'hi' })).toStrictEqual({ a: 'hi' }); + it('strips unknown keys silently (no throw)', () => { + const schema = z.object({ a: z.string() }); + expect(decodeOrThrowZod(schema)({ a: 'hi', b: 1 })).toStrictEqual({ a: 'hi' }); }); - it('returns the object after decoding for rt.strict', () => { - const schemaRt = rt.strict({ - a: rt.string, - }); + it('throws when a required field is missing', () => { + const schema = z.object({ a: z.string() }); + expect(() => decodeOrThrowZod(schema)({})).toThrowErrorMatchingInlineSnapshot( + `"a: Invalid input: expected string, received undefined"` + ); + }); - expect(decodeWithExcessOrThrow(schemaRt)({ a: 'hi' })).toStrictEqual({ a: 'hi' }); + it('uses the provided error factory', () => { + const schema = z.object({ a: z.string() }); + class CustomError extends Error {} + expect(() => decodeOrThrowZod(schema, (m) => new CustomError(m))({})).toThrow(CustomError); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/common/runtime_types.ts b/x-pack/platform/plugins/shared/cases/server/common/runtime_types.ts index 80ac3a9a3d9ab..2d54d96c9b613 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/runtime_types.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/runtime_types.ts @@ -5,41 +5,40 @@ * 2.0. */ -import type * as rt from 'io-ts'; import { badRequest } from '@hapi/boom'; -import { fold } from 'fp-ts/Either'; -import { identity } from 'fp-ts/function'; -import { pipe } from 'fp-ts/pipeable'; - -import { exactCheck } from '@kbn/securitysolution-io-ts-utils/src/exact_check'; -import { formatErrors } from '@kbn/securitysolution-io-ts-utils/src/format_errors'; -import { throwErrors } from '../../common/api'; +import type { ZodType } from '@kbn/zod/v4'; +import { DeepStrict, stringifyZodError } from '@kbn/zod-helpers'; type ErrorFactory = (message: string) => Error; export const createPlainError = (message: string) => new Error(message); -export const throwBadRequestError = (errors: rt.Errors) => { - throw badRequest(formatErrors(errors).join()); -}; - /** - * This function will throw if a required field is missing or an excess field is present. - * NOTE: This will only throw for an excess field if the type passed in leverages exact from io-ts. + * Zod equivalent of `decodeOrThrow` from `./runtime_types`. Parses input + * against the provided schema and throws (via `createError`) when the input + * fails validation. Unknown keys are silently stripped — matching io-ts's + * `decodeOrThrow` behavior on `rt.exact` schemas. (`rt.strict` rejected + * excess keys; for that behavior use `decodeWithExcessOrThrowZod`.) */ -export const decodeWithExcessOrThrow = - (runtimeType: rt.Type) => - (inputValue: I): A => - pipe( - runtimeType.decode(inputValue), - (decoded) => exactCheck(inputValue, decoded), - fold(throwBadRequestError, identity) - ); +export const decodeOrThrowZod = + (schema: ZodType, createError: ErrorFactory = createPlainError) => + (value: unknown): T => { + const result = schema.safeParse(value); + if (result.success) return result.data; + throw createError(stringifyZodError(result.error)); + }; /** - * This function will throw if a required field is missing. + * Zod equivalent of `decodeWithExcessOrThrow` from `./runtime_types`. Wraps + * the schema with `DeepStrict` so any unrecognized keys (at any depth) cause + * validation to fail with `Boom.badRequest` — matching io-ts's `exactCheck` + * behavior. Use this for incoming request bodies / query strings where + * extra keys should be rejected. */ -export const decodeOrThrow = - (runtimeType: rt.Type, createError: ErrorFactory = createPlainError) => - (inputValue: I) => - pipe(runtimeType.decode(inputValue), fold(throwErrors(createError), identity)); +export const decodeWithExcessOrThrowZod = + (schema: ZodType) => + (value: unknown): T => { + const result = DeepStrict(schema).safeParse(value); + if (result.success) return result.data as T; + throw badRequest(stringifyZodError(result.error)); + }; diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/attachments_v1.ts b/x-pack/platform/plugins/shared/cases/server/common/types/attachments_v1.ts index ea16851f4ff65..d37863fa3a069 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/attachments_v1.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/attachments_v1.ts @@ -8,7 +8,10 @@ import type { SavedObject } from '@kbn/core/server'; import type { JsonValue } from '@kbn/utility-types'; import type { AttachmentAttributes } from '../../../common/types/domain'; -import { AttachmentAttributesRt, AttachmentPatchAttributesRt } from '../../../common/types/domain'; +import { + AttachmentAttributesSchema, + AttachmentPatchAttributesSchema, +} from '../../../common/types/domain'; import type { User } from './user'; export interface AttachmentRequestAttributes { @@ -51,5 +54,5 @@ export type AttachmentPersistedAttributes = AttachmentRequestAttributes & export type AttachmentTransformedAttributes = AttachmentAttributes; export type AttachmentSavedObjectTransformed = SavedObject; -export const AttachmentTransformedAttributesRt = AttachmentAttributesRt; -export const AttachmentPartialAttributesRt = AttachmentPatchAttributesRt; +export const AttachmentTransformedAttributesSchema = AttachmentAttributesSchema; +export const AttachmentPartialAttributesSchema = AttachmentPatchAttributesSchema; diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts b/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts index 0fb530ee65986..3323564b26abc 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/case.test.ts @@ -6,18 +6,13 @@ */ import { omit } from 'lodash'; -import { number } from 'io-ts'; import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; -import { - CaseTransformedAttributesRt, - getPartialCaseTransformedAttributesRt, - OwnerRt, -} from './case'; -import { decodeOrThrow } from '../runtime_types'; +import { getPartialCaseTransformedAttributesSchema, OwnerSchema } from './case'; +import { decodeOrThrowZod } from '../runtime_types'; import { CaseSeverity, CaseStatuses } from '../../../common/types/domain'; describe('case types', () => { - describe('getPartialCaseTransformedAttributesRt', () => { + describe('getPartialCaseTransformedAttributesSchema', () => { const theCaseAttributes = { closed_at: null, closed_by: null, @@ -55,61 +50,45 @@ describe('case types', () => { observables: [], }; - const caseTransformedAttributesProps = CaseTransformedAttributesRt.types.reduce( - (acc, type) => ({ ...acc, ...type.type.props, total_comments: number, total_alerts: number }), - {} - ); + const schema = getPartialCaseTransformedAttributesSchema(); - const type = getPartialCaseTransformedAttributesRt(); + it.each(Object.keys(theCaseAttributes))('does not throw if %s is omitted', (key) => { + const theCase = omit(theCaseAttributes, key); + const decoded = schema.parse(theCase); - it.each(Object.keys(caseTransformedAttributesProps))( - 'does not throw if %s is omitted', - (key) => { - const theCase = omit(theCaseAttributes, key); - const decodedRes = type.decode(theCase); - - expect(decodedRes._tag).toEqual('Right'); - // @ts-expect-error: the check above ensures that right exists - expect(decodedRes.right).toEqual(theCase); - } - ); + expect(decoded).toEqual(theCase); + }); - it('removes excess properties', () => { - const decodedRes = type.decode({ description: 'test', 'not-exists': 'excess' }); + it('strips excess properties', () => { + const decoded = schema.parse({ description: 'test', 'not-exists': 'excess' }); - expect(decodedRes._tag).toEqual('Right'); - // @ts-expect-error: the check above ensures that right exists - expect(decodedRes.right).toEqual({ description: 'test' }); + expect(decoded).toEqual({ description: 'test' }); }); - it('does not remove the attachment stats', () => { - const decodedRes = type.decode({ + it('keeps the attachment stats', () => { + const decoded = schema.parse({ description: 'test', total_alerts: 0, total_comments: 0, }); - expect(decodedRes._tag).toEqual('Right'); - // @ts-expect-error: the check above ensures that right exists - expect(decodedRes.right).toEqual({ description: 'test', total_alerts: 0, total_comments: 0 }); + expect(decoded).toEqual({ description: 'test', total_alerts: 0, total_comments: 0 }); }); }); - describe('OwnerRt', () => { + describe('OwnerSchema', () => { it('strips excess fields from the result', () => { - const res = decodeOrThrow(OwnerRt)({ + const res = decodeOrThrowZod(OwnerSchema)({ owner: 'yes', created_at: '123', }); - expect(res).toStrictEqual({ - owner: 'yes', - }); + expect(res).toStrictEqual({ owner: 'yes' }); }); it('throws an error when owner is not present', () => { - expect(() => decodeOrThrow(OwnerRt)({})).toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"owner\\""` + expect(() => decodeOrThrowZod(OwnerSchema)({})).toThrowErrorMatchingInlineSnapshot( + `"owner: Invalid input: expected string, received undefined"` ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/case.ts b/x-pack/platform/plugins/shared/cases/server/common/types/case.ts index 1a831cc3be0fd..c9770d6044f50 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/case.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/case.ts @@ -6,10 +6,9 @@ */ import type { SavedObject } from '@kbn/core-saved-objects-server'; -import type { Type } from 'io-ts'; -import { exact, partial, strict, string, number } from 'io-ts'; +import { z } from '@kbn/zod/v4'; import type { CaseAttributes, Observable } from '../../../common/types/domain'; -import { CaseAttributesRt } from '../../../common/types/domain'; +import { CaseAttributesSchema } from '../../../common/types/domain'; import type { ConnectorPersisted } from './connectors'; import type { ExternalServicePersisted } from './external_service'; import type { User, UserProfile } from './user'; @@ -77,31 +76,20 @@ export type CaseTransformedAttributesWithAttachmentStats = CaseAttributes & { total_events: number; }; -export const CaseTransformedAttributesRt = CaseAttributesRt; +export const CaseTransformedAttributesSchema = CaseAttributesSchema; -export const getPartialCaseTransformedAttributesRt = (): Type< - Partial -> => { - const caseTransformedAttributesProps = CaseAttributesRt.types.reduce( - (acc, type) => Object.assign(acc, type.type.props), - {} - ); - - return exact( - /** - * We add the `total_comments`, `total_alerts`, and `total_events` properties to allow the - * attachments stats to be updated. - */ - partial({ - ...caseTransformedAttributesProps, - total_comments: number, - total_alerts: number, - total_events: number, - }) - ); -}; +/** + * We add the `total_comments`, `total_alerts`, and `total_events` properties to allow the + * attachments stats to be updated. + */ +export const getPartialCaseTransformedAttributesSchema = () => + CaseAttributesSchema.partial().extend({ + total_comments: z.number().optional(), + total_alerts: z.number().optional(), + total_events: z.number().optional(), + }); export type CaseSavedObject = SavedObject; export type CaseSavedObjectTransformed = SavedObject; -export const OwnerRt = strict({ owner: string }); +export const OwnerSchema = z.object({ owner: z.string() }); diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/configure.test.ts b/x-pack/platform/plugins/shared/cases/server/common/types/configure.test.ts index c5cf9711c23d8..2df2d59c27a11 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/configure.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/configure.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { decodeOrThrow } from '../runtime_types'; -import { ConfigurationPartialAttributesRt } from './configure'; +import { decodeOrThrowZod } from '../runtime_types'; +import { ConfigurationPartialAttributesSchema } from './configure'; describe('Configuration', () => { - describe('ConfigurationPartialAttributesRt', () => { + describe('ConfigurationPartialAttributesSchema', () => { it('strips excess fields from the result', () => { - const res = decodeOrThrow(ConfigurationPartialAttributesRt)({ + const res = decodeOrThrowZod(ConfigurationPartialAttributesSchema)({ bananas: 'yes', created_at: '123', }); @@ -22,7 +22,7 @@ describe('Configuration', () => { }); it('should not throw even with an empty object', () => { - expect(() => decodeOrThrow(ConfigurationPartialAttributesRt)({})).not.toThrow(); + expect(() => decodeOrThrowZod(ConfigurationPartialAttributesSchema)({})).not.toThrow(); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/configure.ts b/x-pack/platform/plugins/shared/cases/server/common/types/configure.ts index 0ee185ace3739..a01a32ac5e15c 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/configure.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/configure.ts @@ -5,7 +5,7 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import type { SavedObject } from '@kbn/core/server'; import type { @@ -15,9 +15,9 @@ import type { ConfigurationAttributes, } from '../../../common/types/domain'; import { - ConfigurationActivityFieldsRt, - ConfigurationAttributesRt, - ConfigurationBasicWithoutOwnerRt, + ConfigurationActivityFieldsSchema, + ConfigurationAttributesSchema, + ConfigurationBasicWithoutOwnerSchema, } from '../../../common/types/domain'; import type { ConnectorPersisted } from './connectors'; import type { User, UserProfile } from './user'; @@ -71,14 +71,8 @@ export interface CaseFieldsAttributes { export type ConfigurationTransformedAttributes = ConfigurationAttributes; export type ConfigurationSavedObjectTransformed = SavedObject; -export const ConfigurationPartialAttributesRt = rt.intersection([ - rt.exact(rt.partial(ConfigurationBasicWithoutOwnerRt.type.props)), - rt.exact(rt.partial(ConfigurationActivityFieldsRt.type.props)), - rt.exact( - rt.partial({ - owner: rt.string, - }) - ), -]); +export const ConfigurationPartialAttributesSchema = ConfigurationBasicWithoutOwnerSchema.partial() + .merge(ConfigurationActivityFieldsSchema.partial()) + .merge(z.object({ owner: z.string() }).partial()); -export const ConfigurationTransformedAttributesRt = ConfigurationAttributesRt; +export const ConfigurationTransformedAttributesSchema = ConfigurationAttributesSchema; diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.test.ts b/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.test.ts index ef8834ec552d8..559892a49eaf4 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.test.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { decodeOrThrow } from '../runtime_types'; -import { ConnectorMappingsAttributesPartialRt } from './connector_mappings'; +import { decodeOrThrowZod } from '../runtime_types'; +import { ConnectorMappingsAttributesPartialSchema } from './connector_mappings'; describe('mappings', () => { - describe('ConnectorMappingsAttributesPartialRt', () => { + describe('ConnectorMappingsAttributesPartialSchema', () => { it('strips excess fields from the object', () => { - const res = decodeOrThrow(ConnectorMappingsAttributesPartialRt)({ + const res = decodeOrThrowZod(ConnectorMappingsAttributesPartialSchema)({ bananas: 'yes', owner: 'hi', }); @@ -21,7 +21,7 @@ describe('mappings', () => { }); it('does not throw when the object is empty', () => { - expect(() => decodeOrThrow(ConnectorMappingsAttributesPartialRt)({})).not.toThrow(); + expect(() => decodeOrThrowZod(ConnectorMappingsAttributesPartialSchema)({})).not.toThrow(); }); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.ts b/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.ts index bb6590bd52ec6..cce088bf458ea 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/connector_mappings.ts @@ -5,10 +5,9 @@ * 2.0. */ -import * as rt from 'io-ts'; import type { SavedObject } from '@kbn/core/server'; import type { ConnectorMappingsAttributes } from '../../../common/types/domain'; -import { ConnectorMappingsAttributesRt } from '../../../common/types/domain'; +import { ConnectorMappingsAttributesSchema } from '../../../common/types/domain'; export interface ConnectorMappingsPersistedAttributes { mappings: Array<{ @@ -19,12 +18,10 @@ export interface ConnectorMappingsPersistedAttributes { owner: string; } -export const ConnectorMappingsAttributesTransformedRt = ConnectorMappingsAttributesRt; +export const ConnectorMappingsAttributesTransformedSchema = ConnectorMappingsAttributesSchema; export type ConnectorMappingsAttributesTransformed = ConnectorMappingsAttributes; export type ConnectorMappingsSavedObjectTransformed = SavedObject; -export const ConnectorMappingsAttributesPartialRt = rt.exact( - rt.partial(ConnectorMappingsAttributesRt.type.props) -); +export const ConnectorMappingsAttributesPartialSchema = ConnectorMappingsAttributesSchema.partial(); diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/id_incrementer.ts b/x-pack/platform/plugins/shared/cases/server/common/types/id_incrementer.ts index fb5b407178331..aaf89a1e1dabc 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/id_incrementer.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/id_incrementer.ts @@ -6,7 +6,7 @@ */ import type { SavedObject } from '@kbn/core-saved-objects-server'; -import { CaseIdIncrementerAttributesRt } from '../../../common/types/domain/incremental_id/latest'; +import { CaseIdIncrementerAttributesSchema } from '../../../common/types/domain/incremental_id/v1'; export interface CaseIdIncrementerPersistedAttributes { '@timestamp': number; @@ -16,6 +16,6 @@ export interface CaseIdIncrementerPersistedAttributes { export type CaseIdIncrementerTransformedAttributes = CaseIdIncrementerPersistedAttributes; -export const CaseIdIncrementerTransformedAttributesRt = CaseIdIncrementerAttributesRt; +export const CaseIdIncrementerTransformedAttributesSchema = CaseIdIncrementerAttributesSchema; export type CaseIdIncrementerSavedObject = SavedObject; diff --git a/x-pack/platform/plugins/shared/cases/server/common/types/user_actions.ts b/x-pack/platform/plugins/shared/cases/server/common/types/user_actions.ts index dd17a16331ed7..5125fbe03235a 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/types/user_actions.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/types/user_actions.ts @@ -8,8 +8,8 @@ import type { SavedObject } from '@kbn/core/server'; import type { UserActionAttributes } from '../../../common/types/domain'; import { - UserActionAttributesRt, - CaseUserActionWithoutReferenceIdsRt, + UserActionAttributesSchema, + CaseUserActionWithoutReferenceIdsSchema, } from '../../../common/types/domain'; import type { User } from './user'; @@ -25,8 +25,8 @@ export interface UserActionPersistedAttributes extends UserActionCommonPersisted payload: Record; } -export const UserActionTransformedAttributesRt = UserActionAttributesRt; -export const UserActionPersistedAttributesRt = CaseUserActionWithoutReferenceIdsRt; +export const UserActionTransformedAttributesSchema = UserActionAttributesSchema; +export const UserActionPersistedAttributesSchema = CaseUserActionWithoutReferenceIdsSchema; export type UserActionTransformedAttributes = UserActionAttributes; export type UserActionSavedObjectTransformed = SavedObject; diff --git a/x-pack/platform/plugins/shared/cases/server/common/utils.ts b/x-pack/platform/plugins/shared/cases/server/common/utils.ts index b2bca00a798fb..9309182a1ef94 100644 --- a/x-pack/platform/plugins/shared/cases/server/common/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/common/utils.ts @@ -29,11 +29,11 @@ import type { } from '../../common/types/domain'; import { AttachmentType, - ExternalReferenceSOAttachmentPayloadRt, - FileAttachmentMetadataRt, CaseSeverity, CaseStatuses, ConnectorTypes, + ExternalReferenceSOAttachmentPayloadSchema, + FileAttachmentMetadataSchema, } from '../../common/types/domain'; import { isValidOwner } from '../../common/utils/owner'; import { @@ -319,9 +319,10 @@ export const isPersistableStateOrExternalReference = (context: AttachmentRequest export const isFileAttachmentRequest = ( context: Partial ): context is FileAttachmentRequest => { + const parsed = ExternalReferenceSOAttachmentPayloadSchema.safeParse(context); return ( - ExternalReferenceSOAttachmentPayloadRt.is(context) && - FileAttachmentMetadataRt.is(context.externalReferenceMetadata) + parsed.success && + FileAttachmentMetadataSchema.safeParse(parsed.data.externalReferenceMetadata).success ); }; diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/cases/push_case.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/cases/push_case.ts index bc2e82cf9bf61..268ea534236a8 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/cases/push_case.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { decodeWithExcessOrThrow } from '../../../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../../../common/runtime_types'; import { CASE_PUSH_URL } from '../../../../common/constants'; import type { CaseRoute } from '../types'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; -import { caseApiV1 } from '../../../../common/types/api'; +import { CasePushRequestParamsSchema } from '../../../../common/types/api'; import type { caseDomainV1 } from '../../../../common/types/domain'; import { DEFAULT_CASES_ROUTE_SECURITY } from '../constants'; @@ -28,7 +28,7 @@ export const pushCaseRoute: CaseRoute = createCasesRoute({ const caseContext = await context.cases; const casesClient = await caseContext.getCasesClient(); - const params = decodeWithExcessOrThrow(caseApiV1.CasePushRequestParamsRt)(request.params); + const params = decodeWithExcessOrThrowZod(CasePushRequestParamsSchema)(request.params); const res: caseDomainV1.Case = await casesClient.cases.push({ caseId: params.case_id, connectorId: params.connector_id, diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/comments/patch_comment.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/comments/patch_comment.ts index 3a619a1f767c3..1bf5e95fb6d5e 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/comments/patch_comment.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/comments/patch_comment.ts @@ -6,8 +6,9 @@ */ import { schema } from '@kbn/config-schema'; -import { AttachmentPatchRequestRt } from '../../../../common/types/api'; -import { decodeWithExcessOrThrow } from '../../../common/runtime_types'; +import type { AttachmentPatchRequestV2 } from '../../../../common/types/api'; +import { AttachmentPatchRequestSchema } from '../../../../common/types/api'; +import { decodeWithExcessOrThrowZod } from '../../../common/runtime_types'; import { CASE_COMMENTS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; @@ -32,7 +33,9 @@ export const patchCommentRoute = createCasesRoute({ }, handler: async ({ context, request, response }) => { try { - const query = decodeWithExcessOrThrow(AttachmentPatchRequestRt)(request.body); + const query = decodeWithExcessOrThrowZod(AttachmentPatchRequestSchema)( + request.body + ) as AttachmentPatchRequestV2; const caseContext = await context.cases; const client = await caseContext.getCasesClient(); diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/configure/patch_configure.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/configure/patch_configure.ts index f5f5fd5fbe59c..25c7649fae27a 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/configure/patch_configure.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/configure/patch_configure.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { CaseConfigureRequestParamsRt } from '../../../../common/types/api'; -import { decodeWithExcessOrThrow } from '../../../common/runtime_types'; +import { CaseConfigureRequestParamsSchema } from '../../../../common/types/api'; +import { decodeWithExcessOrThrowZod } from '../../../common/runtime_types'; import { CASE_CONFIGURE_DETAILS_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; @@ -27,7 +27,7 @@ export const patchCaseConfigureRoute = createCasesRoute({ }, handler: async ({ context, request, response }) => { try { - const params = decodeWithExcessOrThrow(CaseConfigureRequestParamsRt)(request.params); + const params = decodeWithExcessOrThrowZod(CaseConfigureRequestParamsSchema)(request.params); const caseContext = await context.cases; const client = await caseContext.getCasesClient(); diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/configure/post_configure.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/configure/post_configure.ts index 32ecc4899ff65..1e3a88248eb06 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/configure/post_configure.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/configure/post_configure.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { ConfigurationRequestRt } from '../../../../common/types/api'; -import { decodeWithExcessOrThrow } from '../../../common/runtime_types'; +import type { ConfigurationRequest, configureApiV1 } from '../../../../common/types/api'; +import { ConfigurationRequestSchema } from '../../../../common/types/api'; +import { decodeWithExcessOrThrowZod } from '../../../common/runtime_types'; import { CASE_CONFIGURE_URL } from '../../../../common/constants'; import { createCaseError } from '../../../common/error'; import { createCasesRoute } from '../create_cases_route'; -import type { configureApiV1 } from '../../../../common/types/api'; import { DEFAULT_CASES_ROUTE_SECURITY } from '../constants'; export const postCaseConfigureRoute = createCasesRoute({ @@ -27,7 +27,9 @@ export const postCaseConfigureRoute = createCasesRoute({ }, handler: async ({ context, request, response }) => { try { - const query = decodeWithExcessOrThrow(ConfigurationRequestRt)(request.body); + const query = decodeWithExcessOrThrowZod(ConfigurationRequestSchema)( + request.body + ) as ConfigurationRequest; const caseContext = await context.cases; const client = await caseContext.getCasesClient(); diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_delete_file_attachments.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_delete_file_attachments.ts index 6ff2ef9848cda..0a9ad359ae10c 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_delete_file_attachments.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_delete_file_attachments.ts @@ -7,13 +7,13 @@ import { schema } from '@kbn/config-schema'; -import { decodeWithExcessOrThrow } from '../../../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../../../common/runtime_types'; import { INTERNAL_DELETE_FILE_ATTACHMENTS_URL } from '../../../../common/constants'; import { createCasesRoute } from '../create_cases_route'; import { createCaseError } from '../../../common/error'; import { escapeHatch } from '../utils'; import type { attachmentApiV1 } from '../../../../common/types/api'; -import { BulkDeleteFileAttachmentsRequestRt } from '../../../../common/types/api/attachment/v1'; +import { BulkDeleteFileAttachmentsRequestSchema } from '../../../../common/types/api'; import { DEFAULT_CASES_ROUTE_SECURITY } from '../constants'; export const bulkDeleteFileAttachments = createCasesRoute({ @@ -33,9 +33,8 @@ export const bulkDeleteFileAttachments = createCasesRoute({ try { const caseContext = await context.cases; const client = await caseContext.getCasesClient(); - const requestBody: attachmentApiV1.BulkDeleteFileAttachmentsRequest = decodeWithExcessOrThrow( - BulkDeleteFileAttachmentsRequestRt - )(request.body); + const requestBody: attachmentApiV1.BulkDeleteFileAttachmentsRequest = + decodeWithExcessOrThrowZod(BulkDeleteFileAttachmentsRequestSchema)(request.body); await client.attachments.bulkDeleteFileAttachments({ caseId: request.params.case_id, diff --git a/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_get_attachments.ts b/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_get_attachments.ts index e8901a8ee09a7..8686086f6bacc 100644 --- a/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_get_attachments.ts +++ b/x-pack/platform/plugins/shared/cases/server/routes/api/internal/bulk_get_attachments.ts @@ -6,8 +6,8 @@ */ import { schema } from '@kbn/config-schema'; -import { BulkGetAttachmentsRequestRt } from '../../../../common/types/api/attachment/v1'; -import { decodeWithExcessOrThrow } from '../../../common/runtime_types'; +import { BulkGetAttachmentsRequestSchema } from '../../../../common/types/api'; +import { decodeWithExcessOrThrowZod } from '../../../common/runtime_types'; import type { attachmentApiV2 } from '../../../../common/types/api'; import { INTERNAL_BULK_GET_ATTACHMENTS_URL } from '../../../../common/constants'; @@ -34,8 +34,8 @@ export const bulkGetAttachmentsRoute = createCasesRoute({ const caseContext = await context.cases; const client = await caseContext.getCasesClient(); - const requestBody: attachmentApiV2.BulkGetAttachmentsRequestV2 = decodeWithExcessOrThrow( - BulkGetAttachmentsRequestRt + const requestBody: attachmentApiV2.BulkGetAttachmentsRequestV2 = decodeWithExcessOrThrowZod( + BulkGetAttachmentsRequestSchema )(request.body); const res: attachmentApiV2.BulkGetAttachmentsResponseV2 = await client.attachments.bulkGet({ diff --git a/x-pack/platform/plugins/shared/cases/server/saved_object_types/migrations/user_actions/connector_id.ts b/x-pack/platform/plugins/shared/cases/server/saved_object_types/migrations/user_actions/connector_id.ts index 8501eab453220..1dff47298d6b8 100644 --- a/x-pack/platform/plugins/shared/cases/server/saved_object_types/migrations/user_actions/connector_id.ts +++ b/x-pack/platform/plugins/shared/cases/server/saved_object_types/migrations/user_actions/connector_id.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type * as rt from 'io-ts'; - import type { SavedObjectMigrationContext, SavedObjectReference, @@ -14,8 +12,12 @@ import type { SavedObjectUnsanitizedDoc, } from '@kbn/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; -import type { CaseAttributes, CaseConnector } from '../../../../common/types/domain'; -import { CaseConnectorRt, ExternalServiceRt } from '../../../../common/types/domain'; +import type { + CaseAttributes, + CaseConnector, + ExternalService as CaseExternalService, +} from '../../../../common/types/domain'; +import { CaseConnectorSchema, ExternalServiceSchema } from '../../../../common/types/domain'; import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, @@ -138,7 +140,7 @@ export function isCreateCaseConnector( return ( isCreateConnector(action, actionFields) && unsafeCase.connector !== undefined && - CaseConnectorRt.is(unsafeCase.connector) + CaseConnectorSchema.safeParse(unsafeCase.connector).success ); } catch { return false; @@ -222,21 +224,22 @@ function isUpdateCaseConnector( actionDetails: unknown ): actionDetails is CaseConnector { try { - return isUpdateConnector(action, actionFields) && CaseConnectorRt.is(actionDetails); + return ( + isUpdateConnector(action, actionFields) && + CaseConnectorSchema.safeParse(actionDetails).success + ); } catch { return false; } } -type CaseExternalService = rt.TypeOf; - function isPushConnector( action: string, actionFields: string[], actionDetails: unknown ): actionDetails is CaseExternalService { try { - return isPush(action, actionFields) && ExternalServiceRt.is(actionDetails); + return isPush(action, actionFields) && ExternalServiceSchema.safeParse(actionDetails).success; } catch { return false; } diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts index f0484b9e18eb9..0c6b40e74973a 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.test.ts @@ -88,7 +88,7 @@ describe('AttachmentService', () => { id: '1', }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"actions\\", and 23 more"` ); }); @@ -105,7 +105,7 @@ describe('AttachmentService', () => { id: '1', }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\",Invalid value \\"undefined\\" supplied to \\"attachmentId\\",Invalid value \\"undefined\\" supplied to \\"data\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"actions\\", and 26 more"` ); }); @@ -188,7 +188,7 @@ describe('AttachmentService', () => { ], }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"actions\\", and 23 more"` ); }); @@ -205,7 +205,7 @@ describe('AttachmentService', () => { attachments: [{ attributes: invalidAttachment.attributes, references: [], id: '1' }], }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\",Invalid value \\"undefined\\" supplied to \\"attachmentId\\",Invalid value \\"undefined\\" supplied to \\"data\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"actions\\", and 26 more"` ); }); @@ -1082,7 +1082,7 @@ describe('AttachmentService', () => { ); await expect(service.find({ mode: 'legacy' })).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"actions\\", and 23 more"` ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts index 1d2811c83fe46..7d72642cab027 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/index.ts @@ -17,12 +17,15 @@ import type { import type { estypes } from '@elastic/elasticsearch'; import { fromKueryExpression } from '@kbn/es-query'; import { AttachmentType } from '../../../common/types/domain'; -import type { AttachmentMode } from '../../../common/types/domain/attachment/v2'; +import type { + AttachmentMode, + AttachmentPatchAttributesV2, +} from '../../../common/types/domain/attachment/v2'; import { - AttachmentAttributesRtV2, - AttachmentPatchAttributesRtV2, + AttachmentAttributesSchemaV2, + AttachmentPatchAttributesSchemaV2, } from '../../../common/types/domain/attachment/v2'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { CASE_ATTACHMENT_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, @@ -67,8 +70,8 @@ import type { AttachmentSavedObjectTransformed, } from '../../common/types/attachments_v1'; import { - AttachmentTransformedAttributesRt, - AttachmentPartialAttributesRt, + AttachmentTransformedAttributesSchema, + AttachmentPartialAttributesSchema, } from '../../common/types/attachments_v1'; import type { AttachmentAttributesV2, @@ -353,7 +356,7 @@ export class AttachmentService { try { this.context.log.debug(`Attempting to POST a new comment`); - const decodedAttributes = decodeOrThrow(AttachmentAttributesRtV2)(attributes); + const decodedAttributes = decodeOrThrowZod(AttachmentAttributesSchemaV2)(attributes); const savedObjectType = getAttachmentSavedObjectType(this.context.config); const transformer = getAttachmentTypeTransformers( getAttachmentTypeFromAttributes(decodedAttributes), @@ -384,7 +387,7 @@ export class AttachmentService { ); // v2 union accepts both unified- and legacy-shape attributes (some // unmigrated types still pass through legacy-shaped). - const validatedAttributes = decodeOrThrow(AttachmentAttributesRtV2)( + const validatedAttributes = decodeOrThrowZod(AttachmentAttributesSchemaV2)( injectedAttachment.attributes ); return Object.assign(injectedAttachment, { @@ -416,7 +419,7 @@ export class AttachmentService { this.context.persistableStateAttachmentTypeRegistry ); - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentTransformedAttributesSchema)( transformedAttachment.attributes ); @@ -440,7 +443,7 @@ export class AttachmentService { const res = await this.context.unsecuredSavedObjectsClient.bulkCreate( attachments.map((attachment) => { - const decodedAttributes = decodeOrThrow(AttachmentAttributesRtV2)( + const decodedAttributes = decodeOrThrowZod(AttachmentAttributesSchemaV2)( attachment.attributes ); const transformer = getAttachmentTypeTransformers( @@ -472,7 +475,7 @@ export class AttachmentService { const res = await this.context.unsecuredSavedObjectsClient.bulkCreate( attachments.map((attachment) => { - const decodedAttributes = decodeOrThrow(AttachmentAttributesRtV2)( + const decodedAttributes = decodeOrThrowZod(AttachmentAttributesSchemaV2)( attachment.attributes ); @@ -524,7 +527,7 @@ export class AttachmentService { this.context.persistableStateAttachmentTypeRegistry ); // v2 union accepts both unified- and legacy-shape attributes. - const validatedAttributes = decodeOrThrow(AttachmentAttributesRtV2)( + const validatedAttributes = decodeOrThrowZod(AttachmentAttributesSchemaV2)( injectedAttachment.attributes ); validatedAttachments.push( @@ -539,7 +542,7 @@ export class AttachmentService { this.context.persistableStateAttachmentTypeRegistry ); - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentTransformedAttributesSchema)( transformedAttachment.attributes ); @@ -565,7 +568,9 @@ export class AttachmentService { throw new Error(`Attachment ${savedObjectId} not found`); } - const decodedAttributes = decodeOrThrow(AttachmentPatchAttributesRtV2)(updatedAttributes); + const decodedAttributes = decodeOrThrowZod(AttachmentPatchAttributesSchemaV2)( + updatedAttributes + ) as AttachmentPatchAttributesV2; assertAlertAttachmentHasRuleName(decodedAttributes as Record); const transformer = getAttachmentTypeTransformers( getAttachmentTypeFromAttributes(decodedAttributes), @@ -622,7 +627,7 @@ export class AttachmentService { ); assertAlertAttachmentHasRuleName(transformedAttachment.attributes as Record); - const validatedAttributes = decodeOrThrow(AttachmentPartialAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentPartialAttributesSchema)( transformedAttachment.attributes ); @@ -651,9 +656,9 @@ export class AttachmentService { const res = await this.context.unsecuredSavedObjectsClient.bulkUpdate( comments.map((c) => { - const decodedAttributes = decodeOrThrow(AttachmentPatchAttributesRtV2)( + const decodedAttributes = decodeOrThrowZod(AttachmentPatchAttributesSchemaV2)( c.updatedAttributes - ); + ) as AttachmentPatchAttributesV2; const transformer = getTransformerForPatchAttributes( decodedAttributes, requestWithoutType @@ -675,9 +680,9 @@ export class AttachmentService { const res = await this.context.unsecuredSavedObjectsClient.bulkUpdate( comments.map((c) => { - const decodedAttributes = decodeOrThrow(AttachmentPatchAttributesRtV2)( + const decodedAttributes = decodeOrThrowZod(AttachmentPatchAttributesSchemaV2)( c.updatedAttributes - ); + ) as AttachmentPatchAttributesV2; assertAlertAttachmentHasRuleName(decodedAttributes as Record); const transformer = getTransformerForPatchAttributes( decodedAttributes, @@ -743,14 +748,14 @@ export class AttachmentService { } else if (attachment.type === CASE_ATTACHMENT_SAVED_OBJECT) { // Saved Objects bulkUpdate may return only the attributes that were sent in the request, not // the full merged document. Match single update(): return the validated patch from the request. - const validatedAttributes = decodeOrThrow(AttachmentPatchAttributesRtV2)( + const validatedAttributes = decodeOrThrowZod(AttachmentPatchAttributesSchemaV2)( comments[i].updatedAttributes - ); + ) as AttachmentPatchAttributesV2; validatedAttachments.push(Object.assign(attachment, { attributes: validatedAttributes })); } else { - const decodedAttributes = decodeOrThrow(AttachmentPatchAttributesRtV2)( + const decodedAttributes = decodeOrThrowZod(AttachmentPatchAttributesSchemaV2)( comments[i].updatedAttributes - ); + ) as AttachmentPatchAttributesV2; const transformer = getTransformerForPatchAttributes(decodedAttributes, requestWithoutType); const legacyAttributes = transformer.toLegacySchema(decodedAttributes); const transformedAttachment = injectAttachmentSOAttributesFromRefsForPatch( @@ -762,7 +767,7 @@ export class AttachmentService { assertAlertAttachmentHasRuleName( transformedAttachment.attributes as Record ); - const validatedAttributes = decodeOrThrow(AttachmentPartialAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentPartialAttributesSchema)( transformedAttachment.attributes ); @@ -806,12 +811,12 @@ export class AttachmentService { mode, }); if (transformed.isUnified) { - const validatedAttributes = decodeOrThrow(AttachmentAttributesRtV2)( + const validatedAttributes = decodeOrThrowZod(AttachmentAttributesSchemaV2)( transformed.attributes ); validatedAttachments.push(Object.assign(injectedSo, { attributes: validatedAttributes })); } else { - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentTransformedAttributesSchema)( transformed.attributes ); diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.test.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.test.ts index 4e720d86fbc31..db35090afd26b 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.test.ts @@ -203,7 +203,7 @@ describe('AttachmentService getter', () => { await expect( attachmentGetter.bulkGet(['1'], mode) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"actions\\", and 23 more"` ); }); }); @@ -290,7 +290,7 @@ describe('AttachmentService getter', () => { await expect( attachmentGetter.getAllDocumentsAttachedToCase({ caseId: '1', owner: 'securitySolution' }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"alertId\\""` + `"type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"security.event\\", Invalid input: expected array, received undefined, Invalid input: expected string, received undefined, Invalid input: expected array, received undefined, and 3 more"` ); }); }); @@ -378,7 +378,7 @@ describe('AttachmentService getter', () => { await expect( attachmentGetter.get({ savedObjectId: '1', mode }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"user\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceMetadata\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceId\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceStorage\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", type: Invalid input: expected \\"actions\\", and 23 more"` ); }); }); @@ -420,7 +420,7 @@ describe('AttachmentService getter', () => { await expect( attachmentGetter.getFileAttachments({ caseId: '1', fileIds: ['1'] }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"comment\\",Invalid value \\"externalReference\\" supplied to \\"type\\",Invalid value \\"undefined\\" supplied to \\"alertId\\",Invalid value \\"undefined\\" supplied to \\"index\\",Invalid value \\"undefined\\" supplied to \\"rule\\",Invalid value \\"undefined\\" supplied to \\"eventId\\",Invalid value \\"undefined\\" supplied to \\"actions\\",Invalid value \\"undefined\\" supplied to \\"externalReferenceAttachmentTypeId\\",Invalid value \\"savedObject\\" supplied to \\"externalReferenceStorage,type\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentTypeId\\",Invalid value \\"undefined\\" supplied to \\"persistableStateAttachmentState\\""` + `"comment: Invalid input: expected string, received undefined, type: Invalid input: expected \\"user\\", type: Invalid input: expected \\"alert\\", rule: Invalid input: expected object, received undefined, type: Invalid input: expected \\"event\\", and 17 more"` ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts index 8a9419392f3dd..222a9910bc720 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/get.ts @@ -11,13 +11,13 @@ import type { estypes } from '@elastic/elasticsearch'; import { FILE_SO_TYPE } from '@kbn/files-plugin/common'; import { toUnifiedAttachmentType } from '../../../../common/utils/attachments'; import { isSOError } from '../../../common/error'; -import { decodeOrThrow } from '../../../common/runtime_types'; +import { decodeOrThrowZod } from '../../../common/runtime_types'; import type { AttachmentPersistedAttributes, AttachmentTransformedAttributes, AttachmentSavedObjectTransformed, } from '../../../common/types/attachments_v1'; -import { AttachmentTransformedAttributesRt } from '../../../common/types/attachments_v1'; +import { AttachmentTransformedAttributesSchema } from '../../../common/types/attachments_v1'; import { CASE_ATTACHMENT_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, @@ -35,7 +35,8 @@ import type { AttachmentTotals, DocumentAttachmentAttributesV2, } from '../../../../common/types/domain'; -import { AttachmentType, DocumentAttachmentAttributesRtV2 } from '../../../../common/types/domain'; +import { AttachmentType } from '../../../../common/types/domain'; +import { DocumentAttachmentAttributesSchemaV2 } from '../../../../common/types/domain/attachment/v2'; import type { AlertIdsAggsResult, BulkOptionalAttributes, @@ -144,7 +145,7 @@ export class AttachmentGetter { ...injectedSo, attributes: transformed.attributes, } as SavedObject; - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentTransformedAttributesSchema)( legacySo.attributes ); validatedAttachments.push(Object.assign(legacySo, { attributes: validatedAttributes })); @@ -194,7 +195,7 @@ export class AttachmentGetter { ...injectedSo, attributes: legacyTransformed.attributes, } as SavedObject; - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentTransformedAttributesSchema)( legacySo.attributes ); @@ -313,7 +314,9 @@ export class AttachmentGetter { response: SavedObjectsFindResponse ): Array> { return response.saved_objects.map((so) => { - const validatedAttributes = decodeOrThrow(DocumentAttachmentAttributesRtV2)(so.attributes); + const validatedAttributes = decodeOrThrowZod(DocumentAttachmentAttributesSchemaV2)( + so.attributes + ); return Object.assign(so, { attributes: validatedAttributes }); }); @@ -478,7 +481,7 @@ export class AttachmentGetter { return Object.assign(injectedRes, { attributes: transformed.attributes }); } - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentTransformedAttributesSchema)( transformed.attributes ); @@ -767,7 +770,7 @@ export class AttachmentGetter { }) as AttachmentSavedObjectTransformedV2; } - const validatedAttributes = decodeOrThrow(AttachmentTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(AttachmentTransformedAttributesSchema)( transformed.attributes ); diff --git a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/utils.ts b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/utils.ts index 65963dafa4187..666fd6f41a5fd 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/attachments/operations/utils.ts @@ -6,14 +6,14 @@ */ import { passThroughTransformer } from '../../../common/attachments/base'; -import { decodeOrThrow } from '../../../common/runtime_types'; +import { decodeOrThrowZod } from '../../../common/runtime_types'; import type { AttachmentPersistedAttributes } from '../../../common/types/attachments_v1'; import type { UnifiedAttachmentAttributes } from '../../../common/types/attachments_v2'; -import { - type AttachmentPatchAttributesV2, - type AttachmentMode, - UnifiedAttachmentAttributesRt, +import type { + AttachmentPatchAttributesV2, + AttachmentMode, } from '../../../../common/types/domain/attachment/v2'; +import { UnifiedAttachmentAttributesSchema } from '../../../../common/types/domain/attachment/v2'; import { isMigratedAttachmentType } from '../../../../common/utils/attachments'; import { getAttachmentTypeFromAttributes, @@ -40,7 +40,9 @@ export function transformAttributesForMode({ if (mode === 'unified' && isMigratedAttachmentType(attachmentType, owner)) { const unifiedAttrs = transformer.toUnifiedSchema(attributes); - const validatedAttributes = decodeOrThrow(UnifiedAttachmentAttributesRt)(unifiedAttrs); + const validatedAttributes = decodeOrThrowZod(UnifiedAttachmentAttributesSchema)( + unifiedAttrs + ) as UnifiedAttachmentAttributes; return { isUnified: true, attributes: validatedAttributes }; } diff --git a/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts index 3f52b93ea64a5..5450fac928231 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/cases/index.test.ts @@ -59,7 +59,7 @@ import type { import { CasePersistedSeverity, CasePersistedStatus, - CaseTransformedAttributesRt, + CaseTransformedAttributesSchema, } from '../../common/types/case'; import type { ConfigType } from '../../config'; @@ -2277,10 +2277,7 @@ describe('CasesService', () => { }); describe('Decoding responses', () => { - const caseTransformedAttributesProps = CaseTransformedAttributesRt.types.reduce( - (acc, type) => ({ ...acc, ...type.type.props }), - {} - ); + const caseTransformedAttributesProps = CaseTransformedAttributesSchema.shape; /** * The following fields are set to a default value if missing: @@ -2350,7 +2347,7 @@ describe('CasesService', () => { await expect( service.getCaseIdsByAlertId({ alertId: '1' }) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"owner\\""` + `"owner: Invalid input: expected string, received undefined"` ); }); @@ -2376,9 +2373,7 @@ describe('CasesService', () => { const attributes = omit({ ...theCase.attributes }, key); unsecuredSavedObjectsClient.get.mockResolvedValue({ ...theCase, attributes }); - await expect(service.getCase({ id: 'a' })).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` - ); + await expect(service.getCase({ id: 'a' })).rejects.toThrow(`${key}: Invalid input`); } ); @@ -2473,7 +2468,7 @@ describe('CasesService', () => { }); await expect(service.getResolveCase({ id: 'a' })).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` + `${key}: Invalid input` ); } ); @@ -2668,7 +2663,7 @@ describe('CasesService', () => { }); await expect(service.getCases({ caseIds: ['a', 'b'] })).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` + `${key}: Invalid input` ); } ); @@ -2772,9 +2767,7 @@ describe('CasesService', () => { unsecuredSavedObjectsClient.find.mockResolvedValue(findMockReturn); - await expect(service.findCases()).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` - ); + await expect(service.findCases()).rejects.toThrow(`${key}: Invalid input`); } ); @@ -2944,7 +2937,7 @@ describe('CasesService', () => { attributes: createCasePostParams({ connector: createJiraConnector() }), id: '1', }) - ).rejects.toThrow(`Invalid value "undefined" supplied to "${key}"`); + ).rejects.toThrow(`${key}: Invalid input`); } ); @@ -3058,7 +3051,7 @@ describe('CasesService', () => { }, ], }) - ).rejects.toThrow(`Invalid value "undefined" supplied to "${key}"`); + ).rejects.toThrow(`${key}: Invalid input`); } ); @@ -3331,7 +3324,7 @@ describe('CasesService', () => { attributes, id: '1', }) - ).rejects.toThrow(`Invalid value "undefined" supplied to "title"`); + ).rejects.toThrow(`title: Invalid input`); }); it('remove excess fields', async () => { @@ -3380,7 +3373,7 @@ describe('CasesService', () => { await expect( service.bulkCreateCases({ cases: [{ id: '1', ...attributes }] }) - ).rejects.toThrow(`Invalid value "undefined" supplied to "title"`); + ).rejects.toThrow(`title: Invalid input`); }); it('remove excess fields', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts b/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts index 366536db80ead..57e855d1e721e 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/cases/index.ts @@ -37,7 +37,7 @@ import { CASE_SAVED_OBJECT, MAX_DOCS_PER_PAGE, } from '../../../common/constants'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import type { SavedObjectFindOptionsKueryNode, SavedObjectsBulkResponseWithErrors, @@ -71,10 +71,10 @@ import type { CaseTransformedAttributes, } from '../../common/types/case'; import { - CaseTransformedAttributesRt, + CaseTransformedAttributesSchema, CasePersistedStatus, - getPartialCaseTransformedAttributesRt, - OwnerRt, + getPartialCaseTransformedAttributesSchema, + OwnerSchema, } from '../../common/types/case'; import type { GetCaseIdsByAlertIdArgs, @@ -101,7 +101,7 @@ import { mergeSearchQuery, } from './utils'; -const PartialCaseTransformedAttributesRt = getPartialCaseTransformedAttributesRt(); +const PartialCaseTransformedAttributesSchema = getPartialCaseTransformedAttributesSchema(); /** * `cases-comments.comment` is a text-analyzed field: we use `match_phrase` so the user's @@ -187,7 +187,7 @@ export class CasesService { const owners: Array> = []; for (const so of response.saved_objects) { - const validatedAttributes = decodeOrThrow(OwnerRt)(so.attributes); + const validatedAttributes = decodeOrThrowZod(OwnerSchema)(so.attributes); owners.push(Object.assign(so, { attributes: validatedAttributes })); } @@ -467,7 +467,9 @@ export class CasesService { ); const res = transformSavedObjectToExternalModel(caseSavedObject); - const decodeRes = decodeOrThrow(CaseTransformedAttributesRt)(res.attributes); + const decodeRes = decodeOrThrowZod(CaseTransformedAttributesSchema)( + res.attributes + ) as CaseTransformedAttributes; return { ...res, @@ -491,7 +493,9 @@ export class CasesService { ); const resolvedSO = transformSavedObjectToExternalModel(resolveCaseResult.saved_object); - const decodeRes = decodeOrThrow(CaseTransformedAttributesRt)(resolvedSO.attributes); + const decodeRes = decodeOrThrowZod(CaseTransformedAttributesSchema)( + resolvedSO.attributes + ) as CaseTransformedAttributes; return { ...resolveCaseResult, @@ -518,7 +522,9 @@ export class CasesService { } const so = Object.assign(theCase, transformSavedObjectToExternalModel(theCase)); - const decodeRes = decodeOrThrow(CaseTransformedAttributesRt)(so.attributes); + const decodeRes = decodeOrThrowZod(CaseTransformedAttributesSchema)( + so.attributes + ) as CaseTransformedAttributes; const soWithDecodedRes = Object.assign(so, { attributes: decodeRes }); return soWithDecodedRes; @@ -545,7 +551,7 @@ export class CasesService { }); const res = transformFindResponseToExternalModel(cases); - const decodeRes = bulkDecodeSOAttributes(res.saved_objects, CaseTransformedAttributesRt); + const decodeRes = bulkDecodeSOAttributes(res.saved_objects, CaseTransformedAttributesSchema); return { ...res, @@ -796,7 +802,9 @@ export class CasesService { try { this.log.debug(`Attempting to create a new case`); - const decodedAttributes = decodeOrThrow(CaseTransformedAttributesRt)(attributes); + const decodedAttributes = decodeOrThrowZod(CaseTransformedAttributesSchema)( + attributes + ) as CaseTransformedAttributes; const transformedAttributes = transformAttributesToESModel(decodedAttributes); transformedAttributes.attributes.total_alerts = 0; @@ -810,7 +818,9 @@ export class CasesService { ); const res = transformSavedObjectToExternalModel(createdCase); - const decodedRes = decodeOrThrow(CaseTransformedAttributesRt)(res.attributes); + const decodedRes = decodeOrThrowZod(CaseTransformedAttributesSchema)( + res.attributes + ) as CaseTransformedAttributes; return { ...res, attributes: decodedRes }; } catch (error) { @@ -827,7 +837,9 @@ export class CasesService { this.log.debug(`Attempting to bulk create cases`); const bulkCreateRequest = cases.map(({ id, ...attributes }) => { - const decodedAttributes = decodeOrThrow(CaseTransformedAttributesRt)(attributes); + const decodedAttributes = decodeOrThrowZod(CaseTransformedAttributesSchema)( + attributes + ) as CaseTransformedAttributes; const { attributes: transformedAttributes, referenceHandler } = transformAttributesToESModel(decodedAttributes); @@ -858,7 +870,9 @@ export class CasesService { } const transformedCase = transformSavedObjectToExternalModel(theCase); - const decodedRes = decodeOrThrow(CaseTransformedAttributesRt)(transformedCase.attributes); + const decodedRes = decodeOrThrowZod(CaseTransformedAttributesSchema)( + transformedCase.attributes + ) as CaseTransformedAttributes; return { ...transformedCase, attributes: decodedRes }; }); @@ -880,9 +894,9 @@ export class CasesService { try { this.log.debug(`Attempting to UPDATE case ${caseId}`); - const decodedAttributes = decodeOrThrow(PartialCaseTransformedAttributesRt)( + const decodedAttributes = decodeOrThrowZod(PartialCaseTransformedAttributesSchema)( updatedAttributes - ); + ) as Partial; const transformedAttributes = transformAttributesToESModel(decodedAttributes); const updatedCase = await this.unsecuredSavedObjectsClient.update( @@ -897,7 +911,9 @@ export class CasesService { ); const res = transformUpdateResponseToExternalModel(updatedCase); - const decodeRes = decodeOrThrow(PartialCaseTransformedAttributesRt)(res.attributes); + const decodeRes = decodeOrThrowZod(PartialCaseTransformedAttributesSchema)( + res.attributes + ) as Partial; return { ...res, @@ -917,9 +933,9 @@ export class CasesService { this.log.debug(`Attempting to UPDATE case ${cases.map((c) => c.caseId).join(', ')}`); const bulkUpdate = cases.map(({ caseId, updatedAttributes, version, originalCase }) => { - const decodedAttributes = decodeOrThrow(PartialCaseTransformedAttributesRt)( + const decodedAttributes = decodeOrThrowZod(PartialCaseTransformedAttributesSchema)( updatedAttributes - ); + ) as Partial; const { attributes, referenceHandler } = transformAttributesToESModel(decodedAttributes); return { @@ -943,7 +959,9 @@ export class CasesService { } const so = Object.assign(theCase, transformUpdateResponseToExternalModel(theCase)); - const decodeRes = decodeOrThrow(PartialCaseTransformedAttributesRt)(so.attributes); + const decodeRes = decodeOrThrowZod(PartialCaseTransformedAttributesSchema)( + so.attributes + ) as Partial; const soWithDecodedRes = Object.assign(so, { attributes: decodeRes }); acc.push(soWithDecodedRes); diff --git a/x-pack/platform/plugins/shared/cases/server/services/configure/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/configure/index.test.ts index 751a6f4c9a25b..2a46b794c56a4 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/configure/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/configure/index.test.ts @@ -960,7 +960,7 @@ describe('CaseConfigureService', () => { attributes, id: '1', }) - ).rejects.toThrow(`Invalid value "undefined" supplied to "closure_type"`); + ).rejects.toThrow(`Invalid input: expected "close-by-user"`); }); it('strips out excess attributes', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/services/configure/index.ts b/x-pack/platform/plugins/shared/cases/server/services/configure/index.ts index 1eadc2a258d28..922693156baa3 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/configure/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/configure/index.ts @@ -15,7 +15,7 @@ import type { import { ACTION_SAVED_OBJECT_TYPE } from '@kbn/actions-plugin/server'; import type { ConfigurationAttributes } from '../../../common/types/domain'; import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants'; import { transformFieldsToESModel, @@ -36,8 +36,8 @@ import type { ConfigurationPersistedAttributes, } from '../../common/types/configure'; import { - ConfigurationPartialAttributesRt, - ConfigurationTransformedAttributesRt, + ConfigurationPartialAttributesSchema, + ConfigurationTransformedAttributesSchema, } from '../../common/types/configure'; export class CaseConfigureService { @@ -95,7 +95,7 @@ export class CaseConfigureService { const validatedConfigs: ConfigurationSavedObjectTransformed[] = []; for (const config of transformedConfigs.saved_objects) { - const validatedAttributes = decodeOrThrow(ConfigurationTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(ConfigurationTransformedAttributesSchema)( config.attributes ); @@ -118,7 +118,9 @@ export class CaseConfigureService { try { this.log.debug(`Attempting to POST a new case configuration`); - const decodedAttributes = decodeOrThrow(ConfigurationTransformedAttributesRt)(attributes); + const decodedAttributes = decodeOrThrowZod(ConfigurationTransformedAttributesSchema)( + attributes + ) as ConfigurationTransformedAttributes; const esConfigInfo = transformAttributesToESModel(decodedAttributes); @@ -148,7 +150,9 @@ export class CaseConfigureService { try { this.log.debug(`Attempting to UPDATE case configuration ${configurationId}`); - const decodedAttributes = decodeOrThrow(ConfigurationPartialAttributesRt)(updatedAttributes); + const decodedAttributes = decodeOrThrowZod(ConfigurationPartialAttributesSchema)( + updatedAttributes + ) as Partial; const esUpdateInfo = transformAttributesToESModel(decodedAttributes); @@ -167,7 +171,7 @@ export class CaseConfigureService { const transformedConfig = transformUpdateResponseToExternalModel(updatedConfiguration); - const validatedAttributes = decodeOrThrow(ConfigurationPartialAttributesRt)( + const validatedAttributes = decodeOrThrowZod(ConfigurationPartialAttributesSchema)( transformedConfig.attributes ); @@ -183,7 +187,7 @@ const transformToExternalAndValidate = ( configuration: SavedObject ) => { const transformedConfig = transformToExternalModel(configuration); - const validatedAttributes = decodeOrThrow(ConfigurationTransformedAttributesRt)( + const validatedAttributes = decodeOrThrowZod(ConfigurationTransformedAttributesSchema)( transformedConfig.attributes ); diff --git a/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.test.ts index f9acc24e73943..2ad9cc3f107b6 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.test.ts @@ -58,7 +58,7 @@ describe('CaseConfigureService', () => { attributes, references: [], }) - ).rejects.toThrow(`Invalid value "undefined" supplied to "mappings"`); + ).rejects.toThrow(`mappings: Invalid input`); }); it('strips out excess attributes', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.ts b/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.ts index 0892f179922a5..d53a553932819 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/connector_mappings/index.ts @@ -24,10 +24,10 @@ import type { ConnectorMappingsAttributesTransformed, } from '../../common/types/connector_mappings'; import { - ConnectorMappingsAttributesPartialRt, - ConnectorMappingsAttributesTransformedRt, + ConnectorMappingsAttributesPartialSchema, + ConnectorMappingsAttributesTransformedSchema, } from '../../common/types/connector_mappings'; -import { decodeOrThrow } from '../../common/runtime_types'; +import { decodeOrThrowZod } from '../../common/runtime_types'; export class ConnectorMappingsService { constructor(private readonly log: Logger) {} @@ -51,7 +51,7 @@ export class ConnectorMappingsService { > = []; for (const mapping of connectorMappings.saved_objects) { - const validatedMapping = decodeOrThrow(ConnectorMappingsAttributesTransformedRt)( + const validatedMapping = decodeOrThrowZod(ConnectorMappingsAttributesTransformedSchema)( mapping.attributes ); @@ -74,7 +74,9 @@ export class ConnectorMappingsService { try { this.log.debug(`Attempting to POST a new connector mappings`); - const decodedAttributes = decodeOrThrow(ConnectorMappingsAttributesTransformedRt)(attributes); + const decodedAttributes = decodeOrThrowZod(ConnectorMappingsAttributesTransformedSchema)( + attributes + ); const connectorMappings = await unsecuredSavedObjectsClient.create( @@ -86,7 +88,7 @@ export class ConnectorMappingsService { } ); - const validatedAttributes = decodeOrThrow(ConnectorMappingsAttributesTransformedRt)( + const validatedAttributes = decodeOrThrowZod(ConnectorMappingsAttributesTransformedSchema)( connectorMappings.attributes ); @@ -109,7 +111,9 @@ export class ConnectorMappingsService { try { this.log.debug(`Attempting to UPDATE connector mappings ${mappingId}`); - const decodedAttributes = decodeOrThrow(ConnectorMappingsAttributesPartialRt)(attributes); + const decodedAttributes = decodeOrThrowZod(ConnectorMappingsAttributesPartialSchema)( + attributes + ); const updatedMappings = await unsecuredSavedObjectsClient.update( @@ -122,7 +126,7 @@ export class ConnectorMappingsService { } ); - const validatedAttributes = decodeOrThrow(ConnectorMappingsAttributesPartialRt)( + const validatedAttributes = decodeOrThrowZod(ConnectorMappingsAttributesPartialSchema)( updatedMappings.attributes ); diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.test.ts index 4f313652065e5..bae2f003848d8 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.test.ts @@ -1865,7 +1865,7 @@ describe('CaseUserActionService', () => { unsecuredSavedObjectsClient.find.mockResolvedValue(findMockReturn); await expect(service.getAll('1')).rejects.toThrowErrorMatchingInlineSnapshot( - `"Invalid value \\"undefined\\" supplied to \\"payload\\""` + `"type: Invalid input: expected \\"create_case\\", payload: Invalid input: expected object, received undefined, type: Invalid input: expected \\"connector\\", payload: Invalid input: expected object, received undefined, type: Invalid input: expected \\"pushed\\", and 28 more"` ); }); @@ -1938,7 +1938,7 @@ describe('CaseUserActionService', () => { soSerializerMock.rawToSavedObject.mockReturnValue(userActionWithOmittedAttribute); await expect(service.getConnectorFieldsBeforeLatestPush('1', pushes)).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` + `Invalid` ); }); @@ -1955,7 +1955,7 @@ describe('CaseUserActionService', () => { soSerializerMock.rawToSavedObject.mockReturnValue(userActionWithOmittedAttribute); await expect(service.getConnectorFieldsBeforeLatestPush('1', pushes)).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,title"' + 'Invalid' ); }); @@ -1975,7 +1975,7 @@ describe('CaseUserActionService', () => { soSerializerMock.rawToSavedObject.mockReturnValue(userActionWithOmittedAttribute); await expect(service.getConnectorFieldsBeforeLatestPush('1', pushes)).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,connector,fields,issueType",Invalid value "{"priority":"high","parent":"2"}" supplied to "payload,connector,fields"' + 'Invalid' ); }); @@ -2039,9 +2039,7 @@ describe('CaseUserActionService', () => { const soFindRes = createSOFindResponse([{ ...userAction, attributes, score: 0 }]); unsecuredSavedObjectsClient.find.mockResolvedValue(soFindRes); - await expect(service.getMostRecentUserAction('123')).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` - ); + await expect(service.getMostRecentUserAction('123')).rejects.toThrow(`Invalid`); }); it('throws if missing attributes from the payload', async () => { @@ -2050,9 +2048,7 @@ describe('CaseUserActionService', () => { const soFindRes = createSOFindResponse([{ ...userAction, attributes, score: 0 }]); unsecuredSavedObjectsClient.find.mockResolvedValue(soFindRes); - await expect(service.getMostRecentUserAction('123')).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,title"' - ); + await expect(service.getMostRecentUserAction('123')).rejects.toThrow('Invalid'); }); it('throws if missing nested attributes from the payload', async () => { @@ -2064,9 +2060,7 @@ describe('CaseUserActionService', () => { const soFindRes = createSOFindResponse([{ ...userAction, attributes, score: 0 }]); unsecuredSavedObjectsClient.find.mockResolvedValue(soFindRes); - await expect(service.getMostRecentUserAction('123')).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,connector,fields,issueType",Invalid value "{"priority":"high","parent":"2"}" supplied to "payload,connector,fields"' - ); + await expect(service.getMostRecentUserAction('123')).rejects.toThrow('Invalid'); }); it('strips out excess attributes', async () => { @@ -2352,9 +2346,7 @@ describe('CaseUserActionService', () => { unsecuredSavedObjectsClient.find.mockResolvedValue({ ...soFindRes, aggregations }); soSerializerMock.rawToSavedObject.mockReturnValue(userActionWithOmittedAttribute); - await expect(service.getCaseConnectorInformation('1')).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` - ); + await expect(service.getCaseConnectorInformation('1')).rejects.toThrow(`Invalid`); }); it('throws if missing attributes from the payload', async () => { @@ -2370,9 +2362,7 @@ describe('CaseUserActionService', () => { unsecuredSavedObjectsClient.find.mockResolvedValue({ ...soFindRes, aggregations }); soSerializerMock.rawToSavedObject.mockReturnValue(userActionWithOmittedAttribute); - await expect(service.getCaseConnectorInformation('1')).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,title"' - ); + await expect(service.getCaseConnectorInformation('1')).rejects.toThrow('Invalid'); }); it('throws if missing nested attributes from the payload', async () => { @@ -2391,9 +2381,7 @@ describe('CaseUserActionService', () => { unsecuredSavedObjectsClient.find.mockResolvedValue({ ...soFindRes, aggregations }); soSerializerMock.rawToSavedObject.mockReturnValue(userActionWithOmittedAttribute); - await expect(service.getCaseConnectorInformation('1')).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,connector,fields,issueType",Invalid value "{"priority":"high","parent":"2"}" supplied to "payload,connector,fields"' - ); + await expect(service.getCaseConnectorInformation('1')).rejects.toThrow('Invalid'); }); it('strips out excess attributes', async () => { @@ -2525,9 +2513,7 @@ describe('CaseUserActionService', () => { pushActionActionWithOmittedAttribute ); - await expect(service.getCaseConnectorInformation('1')).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` - ); + await expect(service.getCaseConnectorInformation('1')).rejects.toThrow(`Invalid`); }); it('throws if missing attributes from the payload', async () => { @@ -2553,9 +2539,7 @@ describe('CaseUserActionService', () => { pushActionActionWithOmittedAttribute ); - await expect(service.getCaseConnectorInformation('1')).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,externalService"' - ); + await expect(service.getCaseConnectorInformation('1')).rejects.toThrow('Invalid'); }); it('throws if missing nested attributes from the payload', async () => { @@ -2584,9 +2568,7 @@ describe('CaseUserActionService', () => { pushActionActionWithOmittedAttribute ); - await expect(service.getCaseConnectorInformation('1')).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,externalService,external_id"' - ); + await expect(service.getCaseConnectorInformation('1')).rejects.toThrow('Invalid'); }); it('strips out excess attributes', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.ts index 426c7c2f098db..bfe53044ea112 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/index.ts @@ -15,7 +15,8 @@ import type { estypes } from '@elastic/elasticsearch'; import type { KueryNode } from '@kbn/es-query'; import type { CaseUserActionDeprecatedResponse } from '../../../common/types/api'; import { AttachmentType, UserActionActions, UserActionTypes } from '../../../common/types/domain'; -import { decodeOrThrow } from '../../common/runtime_types'; +import type { UserActionAttributes } from '../../../common/types/domain'; +import { decodeOrThrowZod } from '../../common/runtime_types'; import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, @@ -46,8 +47,8 @@ import type { UserActionPersistedAttributes, UserActionSavedObjectTransformed, } from '../../common/types/user_actions'; -import { UserActionTransformedAttributesRt } from '../../common/types/user_actions'; -import { CaseUserActionDeprecatedResponseRt } from '../../../common/types/api'; +import { UserActionTransformedAttributesSchema } from '../../common/types/user_actions'; +import { CaseUserActionDeprecatedResponseSchema } from '../../../common/types/api'; import { isCommentAttachmentType } from '../../../common/utils/attachments'; export class CaseUserActionService { @@ -223,7 +224,9 @@ export class CaseUserActionService { this.context.persistableStateAttachmentTypeRegistry ); - const decodeRes = decodeOrThrow(UserActionTransformedAttributesRt)(res.attributes); + const decodeRes = decodeOrThrowZod(UserActionTransformedAttributesSchema)( + res.attributes + ) as UserActionAttributes; const fieldsDoc = Object.assign(res, { attributes: decodeRes, @@ -287,7 +290,9 @@ export class CaseUserActionService { this.context.persistableStateAttachmentTypeRegistry ); - const decodeRes = decodeOrThrow(UserActionTransformedAttributesRt)(res.attributes); + const decodeRes = decodeOrThrowZod(UserActionTransformedAttributesSchema)( + res.attributes + ) as UserActionAttributes; return { ...res, @@ -370,7 +375,9 @@ export class CaseUserActionService { this.context.persistableStateAttachmentTypeRegistry ); - const decodeRes = decodeOrThrow(UserActionTransformedAttributesRt)(res.attributes); + const decodeRes = decodeOrThrowZod(UserActionTransformedAttributesSchema)( + res.attributes + ) as UserActionAttributes; fieldsDoc = { ...res, attributes: decodeRes }; } @@ -417,7 +424,9 @@ export class CaseUserActionService { this.context.persistableStateAttachmentTypeRegistry ); - const decodeRes = decodeOrThrow(UserActionTransformedAttributesRt)(res.attributes); + const decodeRes = decodeOrThrowZod(UserActionTransformedAttributesSchema)( + res.attributes + ) as UserActionAttributes; return { ...res, attributes: decodeRes }; } } @@ -540,7 +549,7 @@ export class CaseUserActionService { const validatedUserActions: Array> = []; for (const so of transformedUserActions.saved_objects) { - const validatedAttributes = decodeOrThrow(CaseUserActionDeprecatedResponseRt)( + const validatedAttributes = decodeOrThrowZod(CaseUserActionDeprecatedResponseSchema)( so.attributes ); diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.test.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.test.ts index 573344e342943..47579d0109cd7 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.test.ts @@ -130,7 +130,7 @@ describe('UserActionPersister', () => { unset(req, 'userAction.payload.connector.fields'); await expect(persister.createUserAction(req)).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,connector,fields"' + 'payload.connector.fields: Invalid input' ); }); @@ -177,7 +177,7 @@ describe('UserActionPersister', () => { persister.bulkCreateUserAction({ userActions: [req], }) - ).rejects.toThrow('Invalid value "undefined" supplied to "owner"'); + ).rejects.toThrow('owner: Invalid input'); }); it('strips out excess attributes', async () => { @@ -222,7 +222,7 @@ describe('UserActionPersister', () => { unset(req, 'attachments[0].owner'); await expect(persister.bulkCreateAttachmentCreation(req)).rejects.toThrow( - 'Invalid value "undefined" supplied to "owner"' + 'owner: Invalid input' ); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.ts index f654a8760e02d..cfc12c09c2fbe 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/create.ts @@ -19,11 +19,11 @@ import type { } from '../../../../common/types/domain'; import { UserActionActions, UserActionTypes } from '../../../../common/types/domain'; import type { UserActionPersistedAttributes } from '../../../common/types/user_actions'; -import { UserActionPersistedAttributesRt } from '../../../common/types/user_actions'; +import { UserActionPersistedAttributesSchema } from '../../../common/types/user_actions'; import { CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT } from '../../../../common/constants'; import { arraysDifference } from '../../../client/utils'; import { isUserActionType } from '../../../../common/utils/user_actions'; -import { decodeOrThrow } from '../../../common/runtime_types'; +import { decodeOrThrowZod } from '../../../common/runtime_types'; import { BuilderFactory } from '../builder_factory'; import type { AddSyncedAlertsCountToUserActionsParams, @@ -490,9 +490,9 @@ export class UserActionPersister { return await this.context.unsecuredSavedObjectsClient.bulkCreate( actions.map((action) => { - const decodedAttributes = decodeOrThrow(UserActionPersistedAttributesRt)( + const decodedAttributes = decodeOrThrowZod(UserActionPersistedAttributesSchema)( action.parameters.attributes - ); + ) as UserActionPersistedAttributes; return { type: CASE_USER_ACTION_SAVED_OBJECT, @@ -595,7 +595,9 @@ export class UserActionPersister { try { this.context.log.debug(`Attempting to POST a new case user action`); - const decodedAttributes = decodeOrThrow(UserActionPersistedAttributesRt)(attributes); + const decodedAttributes = decodeOrThrowZod(UserActionPersistedAttributesSchema)( + attributes + ) as UserActionPersistedAttributes; const res = await this.context.unsecuredSavedObjectsClient.create( CASE_USER_ACTION_SAVED_OBJECT, diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.test.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.test.ts index f69f195fe312a..da252fe09b503 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.test.ts @@ -140,9 +140,7 @@ describe('UserActionsService: Finder', () => { const soFindRes = createSOFindResponse([{ ...userAction, attributes, score: 0 }]); method(soFindRes); - await expect(finder[soMethodName]({ caseId: '1' })).rejects.toThrow( - `Invalid value "undefined" supplied to "${key}"` - ); + await expect(finder[soMethodName]({ caseId: '1' })).rejects.toThrow(`Invalid`); }); it('throws if type is omitted', async () => { @@ -160,9 +158,7 @@ describe('UserActionsService: Finder', () => { const soFindRes = createSOFindResponse([{ ...userAction, attributes, score: 0 }]); method(soFindRes); - await expect(finder[soMethodName]({ caseId: '1' })).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,title"' - ); + await expect(finder[soMethodName]({ caseId: '1' })).rejects.toThrow('Invalid'); }); it('throws if missing nested attributes from the payload', async () => { @@ -171,9 +167,7 @@ describe('UserActionsService: Finder', () => { const soFindRes = createSOFindResponse([{ ...userAction, attributes, score: 0 }]); method(soFindRes); - await expect(finder[soMethodName]({ caseId: '1' })).rejects.toThrow( - 'Invalid value "undefined" supplied to "payload,connector,fields,issueType",Invalid value "{"priority":"high","parent":"2"}" supplied to "payload,connector,fields"' - ); + await expect(finder[soMethodName]({ caseId: '1' })).rejects.toThrow('Invalid'); }); it('strips out excess attributes', async () => { diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.ts index c84e4453dae2d..3f3e68b17ca41 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/operations/find.ts @@ -11,7 +11,7 @@ import type { SavedObjectsFindResponse } from '@kbn/core-saved-objects-api-serve import type { UserActionFindRequestTypes } from '../../../../common/types/api'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../../../routes/api'; import { defaultSortField } from '../../../common/utils'; -import { decodeOrThrow } from '../../../common/runtime_types'; +import { decodeOrThrowZod } from '../../../common/runtime_types'; import { CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, @@ -28,8 +28,8 @@ import type { UserActionTransformedAttributes, } from '../../../common/types/user_actions'; import { bulkDecodeSOAttributes } from '../../utils'; -import { UserActionTransformedAttributesRt } from '../../../common/types/user_actions'; -import type { UserActionType } from '../../../../common/types/domain'; +import { UserActionTransformedAttributesSchema } from '../../../common/types/user_actions'; +import type { UserActionAttributes, UserActionType } from '../../../../common/types/domain'; import { UserActionActions, UserActionTypes, @@ -70,7 +70,7 @@ export class UserActionFinder { const decodeRes = bulkDecodeSOAttributes( res.saved_objects, - UserActionTransformedAttributesRt + UserActionTransformedAttributesSchema ); return { @@ -237,7 +237,9 @@ export class UserActionFinder { this.context.persistableStateAttachmentTypeRegistry ); - const decodeRes = decodeOrThrow(UserActionTransformedAttributesRt)(res.attributes); + const decodeRes = decodeOrThrowZod(UserActionTransformedAttributesSchema)( + res.attributes + ) as UserActionAttributes; return { ...res, diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.test.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.test.ts index 93ef3a2f65df9..aa11b185b9718 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.test.ts @@ -270,7 +270,7 @@ describe('transform', () => { }, }, references: [], - } as typeof base; + } as unknown as typeof base; const transformed = transformer( createSOFindResponse([createUserActionFindSO(userAction)]), diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.ts index 203e9f5feaaae..bdab69f862188 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/transform.ts @@ -164,7 +164,7 @@ const addReferenceIdToPayload = ( const { attachmentId } = userActionAttributes.payload.comment; if (typeof attachmentId === 'string' && attachmentId.length > 0) { - return userAction.attributes.payload; + return userAction.attributes.payload as UserActionAttributes['payload']; } const refId = findReferenceId( @@ -201,7 +201,7 @@ const addReferenceIdToPayload = ( } } - return userAction.attributes.payload; + return userAction.attributes.payload as UserActionAttributes['payload']; }; function getConnectorIdFromReferences( diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_actions/type_guards.ts b/x-pack/platform/plugins/shared/cases/server/services/user_actions/type_guards.ts index 2b0702a615ad9..d4cf64187835a 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_actions/type_guards.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_actions/type_guards.ts @@ -8,28 +8,28 @@ import { isString } from 'lodash'; import type { CaseAssignees, CaseCustomFields, CaseSettings } from '../../../common/types/domain'; import { - CaseAssigneesRt, - CaseCustomFieldsRt, - CaseSettingsRt, - ExtendedFieldsRt, + CaseAssigneesSchema, + CaseCustomFieldsSchema, + CaseSettingsSchema, } from '../../../common/types/domain'; +import { ExtendedFieldsSchema } from '../../../common/types/domain/user_action/extended_fields/v1'; export const isStringArray = (value: unknown): value is string[] => { return Array.isArray(value) && value.every((val) => isString(val)); }; export const isAssigneesArray = (value: unknown): value is CaseAssignees => { - return CaseAssigneesRt.is(value); + return CaseAssigneesSchema.safeParse(value).success; }; export const isCustomFieldsArray = (value: unknown): value is CaseCustomFields => { - return CaseCustomFieldsRt.is(value); + return CaseCustomFieldsSchema.safeParse(value).success; }; export const isCaseSettings = (value: unknown): value is CaseSettings => { - return CaseSettingsRt.is(value); + return CaseSettingsSchema.safeParse(value).success; }; export const isExtendedFields = (value: unknown): value is Record => { - return ExtendedFieldsRt.is(value); + return ExtendedFieldsSchema.safeParse(value).success; }; diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.test.ts b/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.test.ts index e6354187e4f2e..0ee24c382b489 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.test.ts @@ -27,7 +27,7 @@ describe('suggest', () => { }, }) ).rejects.toThrow( - `Failed to retrieve suggested user profiles in service: Error: The size field cannot be more than ${MAX_SUGGESTED_PROFILES}.` + `Failed to retrieve suggested user profiles in service: Error: size: The size field cannot be more than ${MAX_SUGGESTED_PROFILES}.` ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.ts b/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.ts index 7bc57a96105f5..0b729df0a7f8e 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/user_profiles/index.ts @@ -15,8 +15,8 @@ import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/server'; import type { SuggestUserProfilesRequest } from '../../../common/types/api'; -import { SuggestUserProfilesRequestRt } from '../../../common/types/api'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { SuggestUserProfilesRequestSchema } from '../../../common/types/api'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { Operations } from '../../authorization'; import { createCaseError } from '../../common/error'; import { LicensingService } from '../licensing'; @@ -76,7 +76,7 @@ export class UserProfileService { request: KibanaRequest<{}, {}, SuggestUserProfilesRequest> ): Promise { try { - const params = decodeWithExcessOrThrow(SuggestUserProfilesRequestRt)(request.body); + const params = decodeWithExcessOrThrowZod(SuggestUserProfilesRequestSchema)(request.body); const { name, size, owners } = params; diff --git a/x-pack/platform/plugins/shared/cases/server/services/utils.test.ts b/x-pack/platform/plugins/shared/cases/server/services/utils.test.ts index d4c450801d77c..90b5b95d2a2cf 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/utils.test.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/utils.test.ts @@ -5,23 +5,22 @@ * 2.0. */ -import * as rt from 'io-ts'; +import { z } from '@kbn/zod/v4'; import { bulkDecodeSOAttributes } from './utils'; describe('bulkDecodeSOAttributes', () => { - const typeToTest = rt.type({ foo: rt.string }); + const schemaToTest = z.object({ foo: z.string() }); it('decodes a valid SO correctly', () => { const savedObjects = [{ id: '1', attributes: { foo: 'test' } }]; - const res = bulkDecodeSOAttributes([{ id: '1', attributes: { foo: 'test' } }], typeToTest); + const res = bulkDecodeSOAttributes([{ id: '1', attributes: { foo: 'test' } }], schemaToTest); expect(res.get('1')).toEqual(savedObjects[0].attributes); }); it('throws an error when SO is not valid', () => { - // @ts-expect-error - expect(() => bulkDecodeSOAttributes([{ id: '1', attributes: {} }], typeToTest)).toThrowError( - 'Invalid value "undefined" supplied to "foo"' + expect(() => bulkDecodeSOAttributes([{ id: '1', attributes: {} }], schemaToTest)).toThrowError( + 'foo: Invalid input: expected string, received undefined' ); }); }); diff --git a/x-pack/platform/plugins/shared/cases/server/services/utils.ts b/x-pack/platform/plugins/shared/cases/server/services/utils.ts index 4ec0f25cd5a13..e01424e1de981 100644 --- a/x-pack/platform/plugins/shared/cases/server/services/utils.ts +++ b/x-pack/platform/plugins/shared/cases/server/services/utils.ts @@ -5,17 +5,17 @@ * 2.0. */ -import type { Type } from 'io-ts'; -import { decodeOrThrow } from '../common/runtime_types'; +import type { ZodType } from '@kbn/zod/v4'; +import { decodeOrThrowZod } from '../common/runtime_types'; export const bulkDecodeSOAttributes = ( savedObjects: Array<{ id: string; attributes: T }>, - type: Type + schema: ZodType ) => { const decodeRes = new Map(); for (const so of savedObjects) { - decodeRes.set(so.id, decodeOrThrow(type)(so.attributes)); + decodeRes.set(so.id, decodeOrThrowZod(schema)(so.attributes) as T); } return decodeRes; diff --git a/x-pack/platform/plugins/shared/cases/server/workflows/steps/update_case_helpers.ts b/x-pack/platform/plugins/shared/cases/server/workflows/steps/update_case_helpers.ts index 2e56edac9999c..36191272c49d2 100644 --- a/x-pack/platform/plugins/shared/cases/server/workflows/steps/update_case_helpers.ts +++ b/x-pack/platform/plugins/shared/cases/server/workflows/steps/update_case_helpers.ts @@ -8,9 +8,10 @@ import type { KibanaRequest } from '@kbn/core/server'; import { CaseResponseProperties as CaseResponsePropertiesSchema } from '../../../common/bundled-types.gen'; import type { UpdateCaseStepInput } from '../../../common/workflows/steps/update_case'; -import { CasePatchRequestRt } from '../../../common/types/api'; +import type { CasePatchRequest } from '../../../common/types/api'; +import { CasePatchRequestSchema } from '../../../common/types/api'; import type { CasesClient } from '../../client'; -import { decodeWithExcessOrThrow } from '../../common/runtime_types'; +import { decodeWithExcessOrThrowZod } from '../../common/runtime_types'; import { UPDATE_CASE_FAILED_MESSAGE } from './translations'; import { createCasesStepHandler, @@ -52,11 +53,11 @@ export const prepareCasePatch = async ( ) => { const resolvedVersion = await resolveCaseVersion(client, caseId, version); - return decodeWithExcessOrThrow(CasePatchRequestRt)({ + return decodeWithExcessOrThrowZod(CasePatchRequestSchema)({ id: caseId, version: resolvedVersion, ...normalizeCaseStepUpdatesForBulkPatch(updates), - }); + }) as CasePatchRequest; }; export const updateSingleCase = async ( diff --git a/x-pack/platform/plugins/shared/cases/tsconfig.json b/x-pack/platform/plugins/shared/cases/tsconfig.json index 198cc470af590..60c25b1b37070 100644 --- a/x-pack/platform/plugins/shared/cases/tsconfig.json +++ b/x-pack/platform/plugins/shared/cases/tsconfig.json @@ -26,7 +26,6 @@ "@kbn/kibana-utils-plugin", "@kbn/i18n", "@kbn/utility-types", - "@kbn/securitysolution-io-ts-utils", "@kbn/cases-components", "@kbn/es-query", "@kbn/i18n-react", @@ -99,6 +98,7 @@ "@kbn/core-notifications-browser-mocks", "@kbn/react-query", "@kbn/zod", + "@kbn/zod-helpers", "@kbn/securitysolution-es-utils", "@kbn/response-ops-retry-service", "@kbn/openapi-generator", diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics.ts index 555cd653e4d09..fa7c29f94c1cb 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/metrics/get_case_metrics.ts @@ -85,7 +85,7 @@ export default ({ getService }: FtrProviderContext): void => { // casting here because we're expecting an error with a message field })) as unknown as { message: string }; - expect(errorResponse.message).to.contain('Invalid value "bananas" supplied to "features"'); + expect(errorResponse.message).to.contain('Invalid input: expected "alerts.count"'); }); }); diff --git a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/user_actions_get_users.ts b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/user_actions_get_users.ts index b940b1ddd43af..e9a8f5bcd27dc 100644 --- a/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/user_actions_get_users.ts +++ b/x-pack/platform/test/cases_api_integration/security_and_spaces/tests/common/internal/user_actions_get_users.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import type { Cookie } from 'tough-cookie'; import type { UserProfile } from '@kbn/security-plugin/common'; -import { GetCaseUsersResponseRt } from '@kbn/cases-plugin/common/types/api'; +import { GetCaseUsersResponseSchema } from '@kbn/cases-plugin/common/types/api'; import type { Client } from '@elastic/elasticsearch'; import type { SecurityRoleDescriptor } from '@elastic/elasticsearch/lib/api/types'; import { securitySolutionOnlyAllSpacesRole } from '../../../../common/lib/authentication/roles'; @@ -247,7 +247,7 @@ export default ({ getService }: FtrProviderContext): void => { uid: userProfile.uid, }; - GetCaseUsersResponseRt.encode({ + GetCaseUsersResponseSchema.parse({ assignees: [userToValidate], unassignedUsers: [userToValidate], participants: [userToValidate],