diff --git a/packages/kbn-check-saved-objects-cli/current_fields.json b/packages/kbn-check-saved-objects-cli/current_fields.json index 049f1a7dc4d30..14d7257ef765a 100644 --- a/packages/kbn-check-saved-objects-cli/current_fields.json +++ b/packages/kbn-check-saved-objects-cli/current_fields.json @@ -94,10 +94,12 @@ "createdAt", "createdBy", "description", + "destinations", + "destinations.id", + "destinations.type", "name", "updatedAt", - "updatedBy", - "workflow_id" + "updatedBy" ], "alerting_rule": [ "createdAt", diff --git a/packages/kbn-check-saved-objects-cli/current_mappings.json b/packages/kbn-check-saved-objects-cli/current_mappings.json index bba700d1226d2..56b7de23d86a8 100644 --- a/packages/kbn-check-saved-objects-cli/current_mappings.json +++ b/packages/kbn-check-saved-objects-cli/current_mappings.json @@ -334,8 +334,16 @@ "updatedBy": { "type": "keyword" }, - "workflow_id": { - "type": "keyword" + "destinations": { + "type": "object", + "properties": { + "type": { + "type": "keyword" + }, + "id": { + "type": "keyword" + } + } } } }, diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index ce67f50837327..60cb2c4b95d0d 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -1,36 +1,36 @@ pageLoadAssetSize: - actions: 20000 - advancedSettings: 6196 + actions: 89453 + advancedSettings: 55308 agentBuilder: 96771 agentBuilderPlatform: 64202 - aiAssistantManagementSelection: 13590 + aiAssistantManagementSelection: 99345 aiops: 15227 alerting: 22371 alertingVTwo: 362170 apm: 38573 - apmSourcesAccess: 2278 + apmSourcesAccess: 11772 automaticImport: 12162 automaticImportVTwo: 25753 banners: 4087 canvas: 14881 cases: 225900 - charts: 40269 - cloud: 10648 - cloudConnect: 9149 - cloudDataMigration: 5687 - cloudDefend: 8146 - cloudExperiments: 103984 - cloudFullStory: 4752 - cloudLinks: 36333 + charts: 816985 + cloud: 43226 + cloudConnect: 47037 + cloudDataMigration: 26400 + cloudDefend: 65558 + cloudExperiments: 236792 + cloudFullStory: 21134 + cloudLinks: 308042 cloudSecurityPosture: 19743 console: 31191 contentConnectors: 33014 - contentManagement: 8350 + contentManagement: 51579 controls: 10300 core: 556473 - cps: 5930 + cps: 180331 crossClusterReplication: 12662 - customIntegrations: 11715 + customIntegrations: 51323 dashboard: 20000 dashboardAgent: 5135 dashboardMarkdown: 4994 @@ -38,17 +38,17 @@ pageLoadAssetSize: dataQuality: 11469 datasetQuality: 49315 dataSources: 6748 - dataUsage: 8743 + dataUsage: 36780 dataViewEditor: 7735 dataViewFieldEditor: 26024 dataViewManagement: 6250 - dataViews: 65000 + dataViews: 288957 dataVisualizer: 32778 developerToolbar: 4467 devTools: 8109 discover: 28284 discoverEnhanced: 5517 - discoverShared: 2322 + discoverShared: 17308 elasticAssistant: 338870 elasticAssistantSharedState: 4881 embeddable: 16638 @@ -58,52 +58,52 @@ pageLoadAssetSize: esql: 19247 esqlDataGrid: 10209 esUiShared: 101220 - eventAnnotation: 22361 + eventAnnotation: 92471 eventAnnotationListing: 13164 exploratoryView: 45042 - expressionGauge: 15748 - expressionHeatmap: 17563 + expressionGauge: 741272 + expressionHeatmap: 744397 expressionLegacyMetricVis: 11834 - expressionMetricVis: 17559 + expressionMetricVis: 746696 expressionPartitionVis: 29826 - expressions: 105076 + expressions: 475693 expressionTagcloud: 14009 expressionXY: 45000 - features: 4145 + features: 19082 feedback: 7000 - fieldFormats: 64634 + fieldFormats: 296759 fieldsMetadata: 4901 - files: 6037 - filesManagement: 5208 + files: 34007 + filesManagement: 26552 fileUpload: 22957 fleet: 209495 - genAiSettings: 5663 - globalSearch: 6890 - globalSearchBar: 26330 - globalSearchProviders: 4646 + genAiSettings: 25263 + globalSearch: 37669 + globalSearchBar: 231980 + globalSearchProviders: 20180 graph: 9924 - grokdebugger: 5484 + grokdebugger: 27982 home: 13091 imageEmbeddable: 6272 indexLifecycleManagement: 29818 indexManagement: 35522 - inference: 10368 + inference: 144842 infra: 56302 - ingestPipelines: 17866 + ingestPipelines: 106341 inputControlVis: 7660 inspectComponent: 4900 inspector: 17954 - interactiveSetup: 36524 - intercepts: 21066 - kibanaOverview: 6339 - kibanaReact: 21501 - kibanaUsageCollection: 1736 - kibanaUtils: 54161 - kql: 15485 + interactiveSetup: 296528 + intercepts: 135891 + kibanaOverview: 30904 + kibanaReact: 190987 + kibanaUsageCollection: 11529 + kibanaUtils: 278045 + kql: 85570 kubernetesSecurity: 6807 lens: 71718 licenseManagement: 8265 - licensing: 10073 + licensing: 44437 links: 8620 lists: 5093 logsDataAccess: 7900 @@ -112,10 +112,10 @@ pageLoadAssetSize: maintenanceWindows: 8775 management: 12951 maps: 45917 - mapsEms: 6734 + mapsEms: 34798 metricsDataAccess: 44950 ml: 89000 - mockIdpPlugin: 7544 + mockIdpPlugin: 48811 monitoring: 28983 navigation: 11814 newsfeed: 11369 @@ -124,61 +124,61 @@ pageLoadAssetSize: observabilityAgentBuilder: 9172 observabilityAIAssistant: 61180 observabilityAIAssistantApp: 18012 - observabilityAiAssistantManagement: 7126 + observabilityAiAssistantManagement: 31738 observabilityLogsExplorer: 4918 - observabilityOnboarding: 12872 + observabilityOnboarding: 66523 observabilityShared: 75115 osquery: 47422 - painlessLab: 6299 + painlessLab: 26565 presentationPanel: 11418 presentationUtil: 8985 - productDocBase: 5678 - productIntercept: 9860 + productDocBase: 131149 + productIntercept: 42599 profiling: 20716 - reindexService: 3469 - remoteClusters: 10170 + reindexService: 16740 + remoteClusters: 51408 reporting: 46602 - rollup: 12692 - runtimeFields: 11828 - sampleDataIngest: 2640 - savedObjects: 11735 - savedObjectsFinder: 4319 - savedObjectsManagement: 22390 - savedObjectsTagging: 23144 - savedObjectsTaggingOss: 2163 - savedSearch: 11000 - screenshotMode: 2351 - screenshotting: 3252 + rollup: 84166 + runtimeFields: 61361 + sampleDataIngest: 119801 + savedObjects: 77696 + savedObjectsFinder: 24204 + savedObjectsManagement: 107244 + savedObjectsTagging: 129006 + savedObjectsTaggingOss: 11182 + savedSearch: 55572 + screenshotMode: 20135 + screenshotting: 27377 searchAssistant: 6150 searchGettingStarted: 6678 searchHomepage: 7962 searchIndices: 9991 searchInferenceEndpoints: 8071 - searchNavigation: 7404 - searchNotebooks: 18826 + searchNavigation: 37873 + searchNotebooks: 92423 searchPlayground: 12122 - searchprofiler: 6509 + searchprofiler: 29753 searchQueryRules: 6689 searchSynonyms: 6371 security: 79627 securitySolution: 187862 securitySolutionEss: 38689 securitySolutionServerless: 52082 - serverless: 7451 + serverless: 45807 serverlessObservability: 18343 serverlessSearch: 26287 - serverlessWorkplaceAI: 5078 - sessionView: 47912 - share: 58677 + serverlessWorkplaceAI: 17075 + sessionView: 249610 + share: 370319 slo: 36645 - snapshotRestore: 22068 + snapshotRestore: 103374 spaces: 30016 stackAlerts: 31499 stackConnectors: 85421 - streams: 10000 + streams: 63765 streamsApp: 25375 synthetics: 31571 - telemetry: 25755 + telemetry: 138343 telemetryManagementSection: 5522 timelines: 192134 transform: 16515 @@ -187,21 +187,21 @@ pageLoadAssetSize: uiActionsEnhanced: 17373 unifiedDocViewer: 14513 unifiedSearch: 19500 - upgradeAssistant: 6898 + upgradeAssistant: 30483 uptime: 48171 urlDrilldown: 6812 urlForwarding: 7349 usageCollection: 5655 ux: 8376 visDefaultEditor: 35080 - visTypeGauge: 13006 - visTypeHeatmap: 12457 - visTypeMarkdown: 8817 - visTypeMetric: 11966 - visTypePie: 9939 - visTypeTable: 18999 - visTypeTagcloud: 7876 - visTypeTimelion: 12512 + visTypeGauge: 60647 + visTypeHeatmap: 56942 + visTypeMarkdown: 53619 + visTypeMetric: 54808 + visTypePie: 49398 + visTypeTable: 87897 + visTypeTagcloud: 37943 + visTypeTimelion: 65044 visTypeTimeseries: 20000 visTypeVega: 38538 visTypeVislib: 14679 diff --git a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts index cc3fa1f629406..60285f905e630 100644 --- a/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts +++ b/src/core/server/integration_tests/ci_checks/saved_objects/check_registered_types.test.ts @@ -61,7 +61,7 @@ describe('checking migration metadata changes on all registered SO types', () => "action_task_params": "6751dc8a4707a432bc9b90f5a025f183aefc84bca5ec26c29ce6939b24ea81e4", "ad_hoc_run_params": "9c372f2a8f8b468e9b699a6df633c7f14fab7f13216c9ec160813e75bae56098", "alert": "ab52f596c3499231d37ab4c0ee346010789a9a0b9d64d61a6631986e1e62b2aa", - "alerting_notification_policy": "81fc65ed6652cd1196f83b4222f227e8c7a3f646a6e044f63a2d82f12dacbfb0", + "alerting_notification_policy": "e46b4bb9a744d9421988b839d6701f98f4d8d04af842405097a5443f292a8f94", "alerting_rule": "8f80d561d2b3caf07925be4a5fff52d8433ca3f3f204723f6f3e9e32dbce7f42", "alerting_rule_template": "a26521005d8a51af336ec95a2097c4bd073980c050e3c675cec3851acff78fd9", "api_key_pending_invalidation": "c1c0f5cbb1175a7d25c762b290d9d46c04557e4a8ae6a2c7bf77b8fd99b2146d", @@ -298,9 +298,9 @@ describe('checking migration metadata changes on all registered SO types', () => "alert|warning: The SO type owner should ensure these transform functions DO NOT mutate after they are defined.", "==============================================================================================================", "alerting_notification_policy|global: a9d187f26ea3164d4850986b6aedc82e209f668b", - "alerting_notification_policy|mappings: 92cb4995337ed37008b0aaa5922dee023821aee5", + "alerting_notification_policy|mappings: 45ca582231539f8a20d7780837a52c66682acf6c", "alerting_notification_policy|schemas: da39a3ee5e6b4b0d3255bfef95601890afd80709", - "alerting_notification_policy|10.1.0: decd8733c771f6e7dbbf0148ac4b851a496da3c690ae7998a25296d08e750f7a", + "alerting_notification_policy|10.1.0: 8b0fc9a03a07dcb3cc9bdffe5729cfafd601102418dbae2508bf51bc8d8db147", "=====================================================================================================", "alerting_rule|global: e78adb1490c02adb4c705491c87e08332c0f668e", "alerting_rule|mappings: 05a17ab7488d3b86ec25c724f21a19cd7ebb9778", diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/common.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/common.ts new file mode 100644 index 0000000000000..6edd4c90c52d4 --- /dev/null +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/common.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from '@kbn/zod'; +import { validateDuration } from './validation'; + +const durationSchema = z.string().superRefine((value, ctx) => { + const error = validateDuration(value); + if (error) { + ctx.addIssue({ code: z.ZodIssueCode.custom, message: error }); + } +}); + +export { durationSchema }; diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/index.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/index.ts index 2633f684679d0..8a294d6feb408 100644 --- a/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/index.ts +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/index.ts @@ -8,3 +8,5 @@ export * from './rule_data_schema'; export type { RuleResponse } from './rule_response'; export { validateDuration, validateEsqlQuery } from './validation'; +export * from './notification_policy_data_schema'; +export type { NotificationPolicyResponse } from './notification_policy_response'; diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/notification_policy_data_schema.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/notification_policy_data_schema.ts new file mode 100644 index 0000000000000..db56a1ab9da0a --- /dev/null +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/notification_policy_data_schema.ts @@ -0,0 +1,53 @@ +/* + * 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'; +import { durationSchema } from './common'; + +const workflowNotificationPolicyDestinationSchema = z.object({ + type: z.literal('workflow'), + id: z.string(), +}); + +export const notificationPolicyDestinationSchema = z.discriminatedUnion('type', [ + workflowNotificationPolicyDestinationSchema, +]); + +export type NotificationPolicyDestination = z.infer; + +export const createNotificationPolicyDataSchema = z.object({ + name: z.string(), + description: z.string(), + destinations: z + .array(notificationPolicyDestinationSchema) + .min(1, 'At least one destination must be provided'), + matcher: z.string().optional(), + group_by: z.array(z.string()).optional(), + throttle: z.object({ interval: durationSchema }).optional(), +}); + +export type CreateNotificationPolicyData = z.infer; + +export const updateNotificationPolicyDataSchema = z.object({ + name: z.string().optional(), + description: z.string().optional(), + destinations: z + .array(notificationPolicyDestinationSchema) + .min(1, 'At least one destination must be provided') + .optional(), + matcher: z.string().optional(), + group_by: z.array(z.string()).optional(), + throttle: z.object({ interval: durationSchema }).optional(), +}); + +export type UpdateNotificationPolicyData = z.infer; + +export const updateNotificationPolicyBodySchema = updateNotificationPolicyDataSchema.extend({ + version: z.string(), +}); + +export type UpdateNotificationPolicyBody = z.infer; diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/notification_policy_response.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/notification_policy_response.ts new file mode 100644 index 0000000000000..dc224ae308558 --- /dev/null +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/notification_policy_response.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { NotificationPolicyDestination } from './notification_policy_data_schema'; + +export interface NotificationPolicyResponse { + id: string; + version?: string; + name: string; + description: string; + destinations: NotificationPolicyDestination[]; + matcher?: string; + group_by?: string[]; + throttle?: { interval: string }; + createdBy: string | null; + createdAt: string; + updatedBy: string | null; + updatedAt: string; +} diff --git a/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/rule_data_schema.ts b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/rule_data_schema.ts index 5aaf746fca82e..05b3b09ce0083 100644 --- a/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/rule_data_schema.ts +++ b/x-pack/platform/packages/shared/response-ops/alerting-v2-schemas/src/rule_data_schema.ts @@ -6,17 +6,11 @@ */ import { z } from '@kbn/zod'; -import { validateDuration, validateEsqlQuery } from './validation'; +import { validateEsqlQuery } from './validation'; +import { durationSchema } from './common'; /** Primitives */ -const durationSchema = z.string().superRefine((value, ctx) => { - const error = validateDuration(value); - if (error) { - ctx.addIssue({ code: z.ZodIssueCode.custom, message: error }); - } -}); - const esqlQuerySchema = z .string() .min(1) diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/dispatcher.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/dispatcher.test.ts index 458d61860966d..b8171fe763dbd 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/dispatcher.test.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/dispatcher.test.ts @@ -20,6 +20,7 @@ import { type StorageServiceContract, } from '../../services/storage_service/storage_service'; import { DispatcherService, type DispatcherServiceContract } from '../dispatcher'; +import { waitForDataStreamsReady } from './helpers/wait'; import { setupTestServers } from './setup_test_servers'; /** @@ -325,7 +326,7 @@ describe('DispatcherService integration tests', () => { kibanaServer = servers.kibanaServer; esClient = kibanaServer.coreStart.elasticsearch.client.asInternalUser; - await new Promise((res) => setTimeout(res, 5000)); + await waitForDataStreamsReady(esClient, [ALERT_EVENTS_DATA_STREAM, ALERT_ACTIONS_DATA_STREAM]); }); afterAll(async () => { diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/helpers/wait.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/helpers/wait.ts index fb7c1ae7479e4..1ab96989a6731 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/helpers/wait.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/dispatcher/integration_tests/helpers/wait.ts @@ -5,8 +5,10 @@ * 2.0. */ +import type { ElasticsearchClient } from '@kbn/core/server'; import { adminTestUser } from '@kbn/test'; import { getSupertest, type createRoot, type HttpMethod } from '@kbn/core-test-helpers-kbn-server'; +import pRetry from 'p-retry'; type Root = ReturnType; @@ -30,6 +32,31 @@ export const waitForAlertingVTwoSetup = async (root: Root) => { } }; +export const waitForDataStreamsReady = async ( + esClient: ElasticsearchClient, + dataStreamNames: string[] +) => { + const namePattern = dataStreamNames.join(','); + + await pRetry( + async () => { + const response = await esClient.indices.getDataStream({ name: namePattern }); + if (response.data_streams.length < dataStreamNames.length) { + throw new Error( + `Expected ${dataStreamNames.length} data streams, got ${response.data_streams.length}` + ); + } + }, + { retries: 30, minTimeout: 2000, factor: 1 } + ); + + await esClient.cluster.health({ + index: namePattern, + wait_for_status: 'yellow', + timeout: '60s', + }); +}; + export function getSupertestWithAdminUser(root: Root, method: HttpMethod, path: string) { const testUserCredentials = Buffer.from(`${adminTestUser.username}:${adminTestUser.password}`); return getSupertest(root, method, path).set( diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts index 6da879791cf5a..291235d5cd7b5 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/index.ts @@ -6,9 +6,4 @@ */ export { NotificationPolicyClient } from './notification_policy_client'; -export type { - CreateNotificationPolicyData, - CreateNotificationPolicyParams, - NotificationPolicyResponse, - UpdateNotificationPolicyData, -} from './types'; +export type { CreateNotificationPolicyParams, UpdateNotificationPolicyParams } from './types'; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.test.ts index dff37acd6d04d..11255d18161cb 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.test.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.test.ts @@ -6,7 +6,6 @@ */ import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; - import type { UserProfileServiceStart } from '@kbn/core-user-profile-server'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import { @@ -76,7 +75,7 @@ describe('NotificationPolicyClient', () => { data: { name: 'my-policy', description: 'my-policy description', - workflow_id: 'my-workflow', + destinations: [{ type: 'workflow', id: 'my-workflow' }], }, options: { id: 'policy-id-1' }, }); @@ -86,7 +85,7 @@ describe('NotificationPolicyClient', () => { expect.objectContaining({ name: 'my-policy', description: 'my-policy description', - workflow_id: 'my-workflow', + destinations: [{ type: 'workflow', id: 'my-workflow' }], createdBy: 'elastic_profile_uid', updatedBy: 'elastic_profile_uid', createdAt: '2025-01-01T00:00:00.000Z', @@ -101,7 +100,7 @@ describe('NotificationPolicyClient', () => { version: 'WzEsMV0=', name: 'my-policy', description: 'my-policy description', - workflow_id: 'my-workflow', + destinations: [{ type: 'workflow', id: 'my-workflow' }], createdBy: 'elastic_profile_uid', updatedBy: 'elastic_profile_uid', createdAt: '2025-01-01T00:00:00.000Z', @@ -125,7 +124,7 @@ describe('NotificationPolicyClient', () => { data: { name: 'my-policy', description: 'my-policy description', - workflow_id: 'my-workflow', + destinations: [{ type: 'workflow', id: 'my-workflow' }], }, }); @@ -134,7 +133,7 @@ describe('NotificationPolicyClient', () => { expect.objectContaining({ name: 'my-policy', description: 'my-policy description', - workflow_id: 'my-workflow', + destinations: [{ type: 'workflow', id: 'my-workflow' }], }), expect.objectContaining({ overwrite: false, @@ -145,7 +144,23 @@ describe('NotificationPolicyClient', () => { expect(res.id).toEqual(expect.any(String)); expect(res.name).toBe('my-policy'); expect(res.description).toBe('my-policy description'); - expect(res.workflow_id).toBe('my-workflow'); + expect(res.destinations).toEqual([{ type: 'workflow', id: 'my-workflow' }]); + }); + + it('throws 400 when data is invalid', async () => { + await expect( + client.createNotificationPolicy({ + data: { + name: 'my-policy', + description: 'my-policy description', + destinations: [], + }, + }) + ).rejects.toMatchObject({ + output: { statusCode: 400 }, + }); + + expect(mockSavedObjectsClient.create).not.toHaveBeenCalled(); }); it('throws 409 conflict when id already exists', async () => { @@ -161,7 +176,7 @@ describe('NotificationPolicyClient', () => { data: { name: 'my-policy', description: 'my-policy description', - workflow_id: 'my-workflow', + destinations: [{ type: 'workflow', id: 'my-workflow' }], }, options: { id: 'policy-id-conflict' }, }) @@ -176,7 +191,7 @@ describe('NotificationPolicyClient', () => { const existingAttributes: NotificationPolicySavedObjectAttributes = { name: 'test-policy', description: 'test-policy description', - workflow_id: 'test-workflow', + destinations: [{ type: 'workflow', id: 'test-workflow' }], createdBy: 'elastic_profile_uid', createdAt: '2025-01-01T00:00:00.000Z', updatedBy: 'elastic_profile_uid', @@ -220,12 +235,189 @@ describe('NotificationPolicyClient', () => { }); }); + describe('getNotificationPolicies', () => { + it('returns notification policies for multiple ids in input order', async () => { + const firstAttributes: NotificationPolicySavedObjectAttributes = { + name: 'policy-two', + description: 'policy-two description', + destinations: [{ type: 'workflow', id: 'workflow-two' }], + createdBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + }; + const secondAttributes: NotificationPolicySavedObjectAttributes = { + name: 'policy-one', + description: 'policy-one description', + destinations: [{ type: 'workflow', id: 'workflow-one' }], + createdBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + }; + mockSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: 'policy-id-get-2', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: firstAttributes, + references: [], + version: 'WzIsMV0=', + }, + { + id: 'policy-id-get-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: secondAttributes, + references: [], + version: 'WzEsMV0=', + }, + ], + }); + + const res = await client.getNotificationPolicies({ + ids: ['policy-id-get-2', 'policy-id-get-1'], + }); + + expect(res).toEqual([ + { + id: 'policy-id-get-2', + version: 'WzIsMV0=', + ...firstAttributes, + }, + { + id: 'policy-id-get-1', + version: 'WzEsMV0=', + ...secondAttributes, + }, + ]); + }); + + it('returns an empty array when ids are empty', async () => { + const res = await client.getNotificationPolicies({ ids: [] }); + + expect(res).toEqual([]); + }); + + it('ignores missing notification policies and returns found policies', async () => { + const firstAttributes: NotificationPolicySavedObjectAttributes = { + name: 'policy-found-one', + description: 'policy-found-one description', + destinations: [{ type: 'workflow', id: 'workflow-found-one' }], + createdBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + }; + const thirdAttributes: NotificationPolicySavedObjectAttributes = { + name: 'policy-found-three', + description: 'policy-found-three description', + destinations: [{ type: 'workflow', id: 'workflow-found-three' }], + createdBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + }; + mockSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: 'policy-id-get-found-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: firstAttributes, + references: [], + version: 'WzEsMV0=', + }, + { + id: 'policy-id-get-missing', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + error: { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [notification_policy/policy-id-get-missing] not found', + }, + }, + { + id: 'policy-id-get-found-3', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: thirdAttributes, + references: [], + version: 'WzMsMV0=', + }, + ], + }); + + const res = await client.getNotificationPolicies({ + ids: ['policy-id-get-found-1', 'policy-id-get-missing', 'policy-id-get-found-3'], + }); + + expect(res).toEqual([ + { + id: 'policy-id-get-found-1', + version: 'WzEsMV0=', + ...firstAttributes, + }, + { + id: 'policy-id-get-found-3', + version: 'WzMsMV0=', + ...thirdAttributes, + }, + ]); + }); + + it('ignores documents with non-404 errors and returns valid documents', async () => { + const validAttributes: NotificationPolicySavedObjectAttributes = { + name: 'policy-valid', + description: 'policy-valid description', + destinations: [{ type: 'workflow', id: 'workflow-valid' }], + createdBy: 'elastic_profile_uid', + createdAt: '2025-01-01T00:00:00.000Z', + updatedBy: 'elastic_profile_uid', + updatedAt: '2025-01-01T00:00:00.000Z', + }; + mockSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: 'policy-id-valid', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: validAttributes, + references: [], + version: 'WzEsMV0=', + }, + { + id: 'policy-id-error-500', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'Something went wrong', + }, + }, + ], + }); + + const res = await client.getNotificationPolicies({ + ids: ['policy-id-valid', 'policy-id-error-500'], + }); + + expect(res).toEqual([ + { + id: 'policy-id-valid', + version: 'WzEsMV0=', + ...validAttributes, + }, + ]); + }); + }); + describe('updateNotificationPolicy', () => { it('updates a notification policy successfully', async () => { const existingAttributes: NotificationPolicySavedObjectAttributes = { name: 'original-policy', description: 'original-policy description', - workflow_id: 'original-workflow', + destinations: [{ type: 'workflow', id: 'original-workflow' }], createdBy: 'creator_profile_uid', createdAt: '2024-12-01T00:00:00.000Z', updatedBy: 'updater_profile_uid', @@ -247,7 +439,10 @@ describe('NotificationPolicyClient', () => { }); const res = await client.updateNotificationPolicy({ - data: { name: 'updated-policy', workflow_id: 'updated-workflow' }, + data: { + name: 'updated-policy', + destinations: [{ type: 'workflow', id: 'updated-workflow' }], + }, options: { id: 'policy-id-update-1', version: 'WzEsMV0=' }, }); @@ -257,7 +452,7 @@ describe('NotificationPolicyClient', () => { expect.objectContaining({ name: 'updated-policy', description: 'original-policy description', - workflow_id: 'updated-workflow', + destinations: [{ type: 'workflow', id: 'updated-workflow' }], updatedBy: 'elastic_profile_uid', updatedAt: '2025-01-01T00:00:00.000Z', // Preserves original createdBy and createdAt @@ -273,12 +468,26 @@ describe('NotificationPolicyClient', () => { version: 'WzIsMV0=', name: 'updated-policy', description: 'original-policy description', - workflow_id: 'updated-workflow', + destinations: [{ type: 'workflow', id: 'updated-workflow' }], updatedAt: '2025-01-01T00:00:00.000Z', }) ); }); + it('throws 400 when data is invalid', async () => { + await expect( + client.updateNotificationPolicy({ + data: { destinations: [] }, + options: { id: 'policy-id-update-invalid', version: 'WzEsMV0=' }, + }) + ).rejects.toMatchObject({ + output: { statusCode: 400 }, + }); + + expect(mockSavedObjectsClient.get).not.toHaveBeenCalled(); + expect(mockSavedObjectsClient.update).not.toHaveBeenCalled(); + }); + it('throws 404 when notification policy is not found', async () => { mockSavedObjectsClient.get.mockRejectedValueOnce( SavedObjectsErrorHelpers.createGenericNotFoundError( @@ -289,7 +498,7 @@ describe('NotificationPolicyClient', () => { await expect( client.updateNotificationPolicy({ - data: { workflow_id: 'some-workflow' }, + data: { destinations: [{ type: 'workflow', id: 'some-workflow' }] }, options: { id: 'policy-id-update-404', version: 'WzEsMV0=' }, }) ).rejects.toMatchObject({ @@ -301,7 +510,7 @@ describe('NotificationPolicyClient', () => { const existingAttributes: NotificationPolicySavedObjectAttributes = { name: 'original-policy', description: 'original-policy description', - workflow_id: 'original-workflow', + destinations: [{ type: 'workflow', id: 'original-workflow' }], createdBy: 'creator_profile_uid', createdAt: '2024-12-01T00:00:00.000Z', updatedBy: 'updater_profile_uid', @@ -324,7 +533,7 @@ describe('NotificationPolicyClient', () => { await expect( client.updateNotificationPolicy({ - data: { workflow_id: 'new-workflow' }, + data: { destinations: [{ type: 'workflow', id: 'new-workflow' }] }, options: { id: 'policy-id-conflict', version: 'WzEsMV0=' }, }) ).rejects.toMatchObject({ @@ -338,7 +547,7 @@ describe('NotificationPolicyClient', () => { const existingAttributes: NotificationPolicySavedObjectAttributes = { name: 'policy-to-delete', description: 'policy-to-delete description', - workflow_id: 'workflow-to-delete', + destinations: [{ type: 'workflow', id: 'workflow-to-delete' }], createdBy: 'elastic_profile_uid', createdAt: '2025-01-01T00:00:00.000Z', updatedBy: 'elastic_profile_uid', diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts index d65ca0d794526..c3f6b77be99e7 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/notification_policy_client.ts @@ -6,19 +6,21 @@ */ import Boom from '@hapi/boom'; +import type { NotificationPolicyResponse } from '@kbn/alerting-v2-schemas'; +import { + createNotificationPolicyDataSchema, + updateNotificationPolicyDataSchema, +} from '@kbn/alerting-v2-schemas'; import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-server'; import { inject, injectable } from 'inversify'; import { omit } from 'lodash'; +import { stringifyZodError } from '@kbn/zod-helpers'; import { type NotificationPolicySavedObjectAttributes } from '../../saved_objects'; import type { NotificationPolicySavedObjectServiceContract } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service'; import { NotificationPolicySavedObjectService } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service'; import type { UserServiceContract } from '../services/user_service/user_service'; import { UserService } from '../services/user_service/user_service'; -import type { - CreateNotificationPolicyParams, - NotificationPolicyResponse, - UpdateNotificationPolicyParams, -} from './types'; +import type { CreateNotificationPolicyParams, UpdateNotificationPolicyParams } from './types'; @injectable() export class NotificationPolicyClient { @@ -31,13 +33,18 @@ export class NotificationPolicyClient { public async createNotificationPolicy( params: CreateNotificationPolicyParams ): Promise { + const parsed = createNotificationPolicyDataSchema.safeParse(params.data); + if (!parsed.success) { + throw Boom.badRequest( + `Error validating create notification policy data - ${stringifyZodError(parsed.error)}` + ); + } + const userProfileUid = await this.getUserProfileUid(); const now = new Date().toISOString(); const attributes: NotificationPolicySavedObjectAttributes = { - name: params.data.name, - description: params.data.description, - workflow_id: params.data.workflow_id, + ...parsed.data, createdBy: userProfileUid, createdAt: now, updatedBy: userProfileUid, @@ -72,9 +79,36 @@ export class NotificationPolicyClient { } } + public async getNotificationPolicies({ + ids, + }: { + ids: string[]; + }): Promise { + if (ids.length === 0) { + return []; + } + + const docs = await this.notificationPolicySavedObjectService.bulkGetByIds(ids); + + return docs.flatMap((doc) => { + if ('error' in doc) { + return []; + } + + return [{ id: doc.id, version: doc.version, ...doc.attributes }]; + }); + } + public async updateNotificationPolicy( params: UpdateNotificationPolicyParams ): Promise { + const parsed = updateNotificationPolicyDataSchema.safeParse(params.data); + if (!parsed.success) { + throw Boom.badRequest( + `Error validating update notification policy data - ${stringifyZodError(parsed.error)}` + ); + } + const userProfileUid = await this.getUserProfileUid(); const now = new Date().toISOString(); @@ -86,7 +120,7 @@ export class NotificationPolicyClient { const nextAttrs: NotificationPolicySavedObjectAttributes = { ...existingAttrs, - ...params.data, + ...parsed.data, updatedBy: userProfileUid, updatedAt: now, }; diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts index 88b50b45213e3..8465df2209cf7 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/notification_policy_client/types.ts @@ -5,17 +5,10 @@ * 2.0. */ -export interface CreateNotificationPolicyData { - name: string; - description: string; - workflow_id: string; -} - -export interface UpdateNotificationPolicyData { - name?: string; - description?: string; - workflow_id?: string; -} +import type { + CreateNotificationPolicyData, + UpdateNotificationPolicyData, +} from '@kbn/alerting-v2-schemas'; export interface UpdateNotificationPolicyParams { data: UpdateNotificationPolicyData; @@ -26,15 +19,3 @@ export interface CreateNotificationPolicyParams { data: CreateNotificationPolicyData; options?: { id?: string }; } - -export interface NotificationPolicyResponse { - id: string; - version?: string; - name: string; - description: string; - workflow_id: string; - createdBy: string | null; - createdAt: string; - updatedBy: string | null; - updatedAt: string | null; -} diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.test.ts new file mode 100644 index 0000000000000..fc40473db93bc --- /dev/null +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.test.ts @@ -0,0 +1,273 @@ +/* + * 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 type { SavedObjectsClientContract } from '@kbn/core/server'; +import type { NotificationPolicySavedObjectAttributes } from '../../../saved_objects'; +import { NOTIFICATION_POLICY_SAVED_OBJECT_TYPE } from '../../../saved_objects'; +import type { NotificationPolicySavedObjectService } from './notification_policy_saved_object_service'; +import { createNotificationPolicySavedObjectService } from './notification_policy_saved_object_service.mock'; + +const mockAttrs: NotificationPolicySavedObjectAttributes = { + name: 'test-policy', + description: 'A test notification policy', + destinations: [{ type: 'workflow', id: 'workflow-1' }], + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2025-01-01T00:00:00Z', + updatedAt: '2025-01-01T00:00:00Z', +}; + +describe('NotificationPolicySavedObjectService', () => { + let service: NotificationPolicySavedObjectService; + let mockSoClient: jest.Mocked; + + beforeEach(() => { + const mocks = createNotificationPolicySavedObjectService(); + service = mocks.notificationPolicySavedObjectService; + mockSoClient = mocks.mockSavedObjectsClient; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('creates a saved object with the provided id', async () => { + mockSoClient.create.mockResolvedValue({ + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v1', + }); + + const result = await service.create({ attrs: mockAttrs, id: 'policy-1' }); + + expect(result).toEqual({ id: 'policy-1', version: 'v1' }); + expect(mockSoClient.create).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + mockAttrs, + { id: 'policy-1', overwrite: false } + ); + }); + + it('generates an id when none is provided', async () => { + mockSoClient.create.mockResolvedValue({ + id: expect.any(String), + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v1', + }); + + await service.create({ attrs: mockAttrs }); + + expect(mockSoClient.create).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + mockAttrs, + { id: expect.any(String), overwrite: false } + ); + }); + + it('propagates errors from the saved objects client', async () => { + mockSoClient.create.mockRejectedValue(new Error('conflict')); + + await expect(service.create({ attrs: mockAttrs })).rejects.toThrow('conflict'); + }); + }); + + describe('get', () => { + it('returns id, attributes, and version', async () => { + mockSoClient.get.mockResolvedValue({ + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v1', + }); + + const result = await service.get('policy-1'); + + expect(result).toEqual({ id: 'policy-1', attributes: mockAttrs, version: 'v1' }); + expect(mockSoClient.get).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-1', + undefined + ); + }); + + it('passes namespace when spaceId is provided', async () => { + mockSoClient.get.mockResolvedValue({ + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v1', + }); + + await service.get('policy-1', 'custom-space'); + + expect(mockSoClient.get).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-1', + { namespace: 'custom-space' } + ); + }); + + it('does not pass namespace when spaceId is omitted', async () => { + mockSoClient.get.mockResolvedValue({ + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + }); + + await service.get('policy-1'); + + expect(mockSoClient.get).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-1', + undefined + ); + }); + }); + + describe('update', () => { + it('updates the saved object and returns id and version', async () => { + mockSoClient.update.mockResolvedValue({ + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v2', + }); + + const result = await service.update({ id: 'policy-1', attrs: mockAttrs, version: 'v1' }); + + expect(result).toEqual({ id: 'policy-1', version: 'v2' }); + expect(mockSoClient.update).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-1', + mockAttrs, + { version: 'v1' } + ); + }); + }); + + describe('delete', () => { + it('deletes the saved object by id', async () => { + mockSoClient.delete.mockResolvedValue({}); + + await service.delete({ id: 'policy-1' }); + + expect(mockSoClient.delete).toHaveBeenCalledWith( + NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + 'policy-1' + ); + }); + }); + + describe('bulkGetByIds', () => { + it('returns empty array when ids is empty without calling the client', async () => { + const result = await service.bulkGetByIds([]); + + expect(result).toEqual([]); + expect(mockSoClient.bulkGet).not.toHaveBeenCalled(); + }); + + it('maps successful saved objects to id, attributes, and version', async () => { + mockSoClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v1', + }, + ], + }); + + const result = await service.bulkGetByIds(['policy-1']); + + expect(result).toEqual([{ id: 'policy-1', attributes: mockAttrs, version: 'v1' }]); + expect(mockSoClient.bulkGet).toHaveBeenCalledWith( + [{ type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, id: 'policy-1' }], + undefined + ); + }); + + it('maps saved objects with errors to id and error', async () => { + const soError = { statusCode: 404, error: 'Not Found', message: 'Not found' }; + mockSoClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { + id: 'policy-missing', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + error: soError, + }, + ], + }); + + const result = await service.bulkGetByIds(['policy-missing']); + + expect(result).toEqual([{ id: 'policy-missing', error: soError }]); + }); + + it('handles mixed success and error results', async () => { + const soError = { statusCode: 404, error: 'Not Found', message: 'Not found' }; + mockSoClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v1', + }, + { + id: 'policy-missing', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: {} as NotificationPolicySavedObjectAttributes, + references: [], + error: soError, + }, + ], + }); + + const result = await service.bulkGetByIds(['policy-1', 'policy-missing']); + + expect(result).toEqual([ + { id: 'policy-1', attributes: mockAttrs, version: 'v1' }, + { id: 'policy-missing', error: soError }, + ]); + }); + + it('passes namespace when spaceId is provided', async () => { + mockSoClient.bulkGet.mockResolvedValue({ + saved_objects: [ + { + id: 'policy-1', + type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, + attributes: mockAttrs, + references: [], + version: 'v1', + }, + ], + }); + + await service.bulkGetByIds(['policy-1'], 'custom-space'); + + expect(mockSoClient.bulkGet).toHaveBeenCalledWith( + [{ type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, id: 'policy-1' }], + { namespace: 'custom-space' } + ); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts index c1a13a15f515d..5bf9e8f106153 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/notification_policy_saved_object_service/notification_policy_saved_object_service.ts @@ -6,17 +6,29 @@ */ import { PluginStart } from '@kbn/core-di'; -import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; import type { ISavedObjectsClientFactory } from '@kbn/core-di-server'; import { SavedObjectsClientFactory } from '@kbn/core-di-server'; -import { inject, injectable } from 'inversify'; import type { SavedObjectsClientContract } from '@kbn/core/server'; import { SavedObjectsUtils } from '@kbn/core/server'; -import { NOTIFICATION_POLICY_SAVED_OBJECT_TYPE } from '../../../saved_objects'; +import type { SavedObjectError } from '@kbn/core/types'; +import type { SpacesPluginStart } from '@kbn/spaces-plugin/server'; +import { inject, injectable } from 'inversify'; import type { NotificationPolicySavedObjectAttributes } from '../../../saved_objects'; +import { NOTIFICATION_POLICY_SAVED_OBJECT_TYPE } from '../../../saved_objects'; import type { AlertingServerStartDependencies } from '../../../types'; import { spaceIdToNamespace } from '../../space_id_to_namespace'; +export type NotificationPolicySavedObjectBulkGetItem = + | { + id: string; + attributes: NotificationPolicySavedObjectAttributes; + version?: string; + } + | { + id: string; + error: SavedObjectError; + }; + export interface NotificationPolicySavedObjectServiceContract { create(params: { attrs: NotificationPolicySavedObjectAttributes; @@ -32,6 +44,10 @@ export interface NotificationPolicySavedObjectServiceContract { version: string; }): Promise<{ id: string; version?: string }>; delete(params: { id: string }): Promise; + bulkGetByIds( + ids: string[], + spaceId?: string + ): Promise; } @injectable() @@ -107,6 +123,36 @@ export class NotificationPolicySavedObjectService return { id: result.id, version: result.version }; } + public async bulkGetByIds( + ids: string[], + spaceId?: string + ): Promise { + if (ids.length === 0) { + return []; + } + + const namespace = spaceIdToNamespace(this.spaces, spaceId); + const result = await this.client.bulkGet( + ids.map((id) => ({ type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, id })), + namespace ? { namespace } : undefined + ); + + return result.saved_objects.map((savedObject) => { + if ('error' in savedObject && savedObject.error) { + return { + id: savedObject.id, + error: savedObject.error, + }; + } + + return { + id: savedObject.id, + attributes: savedObject.attributes, + version: savedObject.version, + }; + }); + } + public async delete({ id }: { id: string }): Promise { await this.client.delete(NOTIFICATION_POLICY_SAVED_OBJECT_TYPE, id); } diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/create_notification_policy_route.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/create_notification_policy_route.ts index cebe044b15a34..5de28f9fcc046 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/create_notification_policy_route.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/create_notification_policy_route.ts @@ -6,6 +6,10 @@ */ import Boom from '@hapi/boom'; +import { + createNotificationPolicyDataSchema, + type CreateNotificationPolicyData, +} from '@kbn/alerting-v2-schemas'; import { Logger } from '@kbn/core-di'; import type { RouteHandler } from '@kbn/core-di-server'; import { Request, Response } from '@kbn/core-di-server'; @@ -22,12 +26,6 @@ const createNotificationPolicyParamsSchema = z.object({ id: z.string().optional(), }); -const createNotificationPolicyBodySchema = z.object({ - name: z.string(), - description: z.string(), - workflow_id: z.string(), -}); - @injectable() export class CreateNotificationPolicyRoute implements RouteHandler { static method = 'post' as const; @@ -40,7 +38,7 @@ export class CreateNotificationPolicyRoute implements RouteHandler { static options = { access: 'internal' } as const; static validate = { request: { - body: buildRouteValidationWithZod(createNotificationPolicyBodySchema), + body: buildRouteValidationWithZod(createNotificationPolicyDataSchema), params: buildRouteValidationWithZod(createNotificationPolicyParamsSchema), }, } as const; @@ -51,7 +49,7 @@ export class CreateNotificationPolicyRoute implements RouteHandler { private readonly request: KibanaRequest< z.infer, unknown, - z.infer + CreateNotificationPolicyData >, @inject(Response) private readonly response: KibanaResponseFactory, @inject(NotificationPolicyClient) diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/update_notification_policy_route.ts b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/update_notification_policy_route.ts index 63972a9743cc0..0230ca01e3ae1 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/update_notification_policy_route.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/routes/notification_policies/update_notification_policy_route.ts @@ -10,7 +10,10 @@ import { Request, Response } from '@kbn/core-di-server'; import type { KibanaRequest, KibanaResponseFactory, RouteSecurity } from '@kbn/core-http-server'; import { z } from '@kbn/zod'; import { inject, injectable } from 'inversify'; -import { omit } from 'lodash'; +import { + updateNotificationPolicyBodySchema, + type UpdateNotificationPolicyBody, +} from '@kbn/alerting-v2-schemas'; import { NotificationPolicyClient } from '../../lib/notification_policy_client'; import { ALERTING_V2_API_PRIVILEGES } from '../../lib/security/privileges'; import { INTERNAL_ALERTING_V2_NOTIFICATION_POLICY_API_PATH } from '../constants'; @@ -20,13 +23,6 @@ const updateNotificationPolicyParamsSchema = z.object({ id: z.string(), }); -const updateNotificationPolicyBodySchema = z.object({ - name: z.string().optional(), - description: z.string().optional(), - workflow_id: z.string().optional(), - version: z.string(), -}); - @injectable() export class UpdateNotificationPolicyRoute { static method = 'put' as const; @@ -49,7 +45,7 @@ export class UpdateNotificationPolicyRoute { private readonly request: KibanaRequest< z.infer, unknown, - z.infer + UpdateNotificationPolicyBody >, @inject(Response) private readonly response: KibanaResponseFactory, @inject(NotificationPolicyClient) @@ -58,9 +54,10 @@ export class UpdateNotificationPolicyRoute { async handle() { try { + const { version, ...data } = this.request.body; const updated = await this.notificationPolicyClient.updateNotificationPolicy({ - data: omit(this.request.body, ['version']), - options: { id: this.request.params.id, version: this.request.body.version }, + data, + options: { id: this.request.params.id, version }, }); return this.response.ok({ body: updated }); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/notification_policy_mappings.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/notification_policy_mappings.ts index cb6dd4e9241e1..9de9688f8aa80 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/notification_policy_mappings.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/notification_policy_mappings.ts @@ -15,7 +15,13 @@ export const notificationPolicyMappings: SavedObjectsTypeMappingDefinition = { properties: { name: { type: 'text', fields: { keyword: { type: 'keyword', ignore_above: 256 } } }, description: { type: 'text', fields: { keyword: { type: 'keyword', ignore_above: 256 } } }, - workflow_id: { type: 'keyword' }, + destinations: { + type: 'object', + properties: { + type: { type: 'keyword' }, + id: { type: 'keyword' }, + }, + }, createdBy: { type: 'keyword' }, createdAt: { type: 'date' }, updatedBy: { type: 'keyword' }, diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/v1.ts b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/v1.ts index e93e4874fcef1..fcfe43329caa5 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/v1.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/saved_objects/schemas/notification_policy_saved_object_attributes/v1.ts @@ -7,15 +7,29 @@ import { schema } from '@kbn/config-schema'; +export const notificationPolicyDestinationSchema = schema.oneOf([ + schema.object({ + type: schema.literal('workflow'), + id: schema.string(), + }), +]); + /** * Attributes for the notification policy saved object. */ export const notificationPolicySavedObjectAttributesSchema = schema.object({ name: schema.string(), description: schema.string(), - workflow_id: schema.string(), + destinations: schema.arrayOf(notificationPolicyDestinationSchema), + matcher: schema.maybe(schema.string()), + group_by: schema.maybe(schema.arrayOf(schema.string())), + throttle: schema.maybe( + schema.object({ + interval: schema.string(), + }) + ), createdBy: schema.nullable(schema.string()), updatedBy: schema.nullable(schema.string()), createdAt: schema.string(), - updatedAt: schema.nullable(schema.string()), + updatedAt: schema.string(), }); diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/create_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/create_notification_policy.ts index fe0f8273d8a6f..b61ab517de555 100644 --- a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/create_notification_policy.ts +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/create_notification_policy.ts @@ -38,7 +38,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'my-policy', description: 'my-policy description', - workflow_id: 'my-workflow-id', + destinations: [{ type: 'workflow', id: 'my-workflow-id' }], + matcher: "env == 'production' && region == 'us-east-1'", + group_by: ['service.name', 'environment'], + throttle: { interval: '1m' }, }); expect(response.status).to.be(200); @@ -46,7 +49,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(response.body.version).to.be.a('string'); expect(response.body.name).to.be('my-policy'); expect(response.body.description).to.be('my-policy description'); - expect(response.body.workflow_id).to.be('my-workflow-id'); + expect(response.body.destinations).to.eql([{ type: 'workflow', id: 'my-workflow-id' }]); + expect(response.body.matcher).to.be("env == 'production' && region == 'us-east-1'"); + expect(response.body.group_by).to.eql(['service.name', 'environment']); + expect(response.body.throttle).to.eql({ interval: '1m' }); expect(response.body.createdAt).to.be.a('string'); expect(response.body.updatedAt).to.be.a('string'); }); @@ -61,14 +67,20 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'another-policy', description: 'another-policy description', - workflow_id: 'another-workflow-id', + destinations: [{ type: 'workflow', id: 'another-workflow-id' }], + matcher: "env == 'staging' && region == 'eu-west-1'", + group_by: ['kubernetes.namespace'], + throttle: { interval: '5m' }, }); expect(response.status).to.be(200); expect(response.body.id).to.be(customId); expect(response.body.name).to.be('another-policy'); expect(response.body.description).to.be('another-policy description'); - expect(response.body.workflow_id).to.be('another-workflow-id'); + expect(response.body.destinations).to.eql([{ type: 'workflow', id: 'another-workflow-id' }]); + expect(response.body.matcher).to.be("env == 'staging' && region == 'eu-west-1'"); + expect(response.body.group_by).to.eql(['kubernetes.namespace']); + expect(response.body.throttle).to.eql({ interval: '5m' }); }); it('should return 409 when creating a notification policy with an existing id', async () => { @@ -79,7 +91,11 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(`${NOTIFICATION_POLICY_API_PATH}/${existingId}`) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send({ name: 'policy-1', description: 'policy-1 description', workflow_id: 'workflow-1' }); + .send({ + name: 'policy-1', + description: 'policy-1 description', + destinations: [{ type: 'workflow', id: 'workflow-1' }], + }); expect(firstResponse.status).to.be(200); @@ -88,7 +104,11 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(`${NOTIFICATION_POLICY_API_PATH}/${existingId}`) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send({ name: 'policy-2', description: 'policy-2 description', workflow_id: 'workflow-2' }); + .send({ + name: 'policy-2', + description: 'policy-2 description', + destinations: [{ type: 'workflow', id: 'workflow-2' }], + }); expect(secondResponse.status).to.be(409); }); @@ -98,7 +118,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(NOTIFICATION_POLICY_API_PATH) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send({ description: 'my-policy description', workflow_id: 'my-workflow-id' }); + .send({ + description: 'my-policy description', + destinations: [{ type: 'workflow', id: 'my-workflow-id' }], + }); expect(response.status).to.be(400); }); @@ -108,12 +131,15 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .post(NOTIFICATION_POLICY_API_PATH) .set(roleAuthc.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send({ name: 'my-policy', workflow_id: 'my-workflow-id' }); + .send({ + name: 'my-policy', + destinations: [{ type: 'workflow', id: 'my-workflow-id' }], + }); expect(response.status).to.be(400); }); - it('should return 400 when workflow_id is missing', async () => { + it('should return 400 when destinations are missing', async () => { const response = await supertestWithoutAuth .post(NOTIFICATION_POLICY_API_PATH) .set(roleAuthc.apiKeyHeader) @@ -122,5 +148,20 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(response.status).to.be(400); }); + + it('should return 400 when throttle interval is invalid', async () => { + const response = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'my-policy', + description: 'my-policy description', + destinations: [{ type: 'workflow', id: 'my-workflow-id' }], + throttle: { interval: 'invalid-interval' }, + }); + + expect(response.status).to.be(400); + }); }); } diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/delete_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/delete_notification_policy.ts index 6775a3006f96f..1bd0ddb1570ec 100644 --- a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/delete_notification_policy.ts +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/delete_notification_policy.ts @@ -38,7 +38,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'test-policy-for-delete', description: 'test-policy-for-delete description', - workflow_id: 'test-workflow-for-delete', + destinations: [{ type: 'workflow', id: 'test-workflow-for-delete' }], }); const createdPolicyId = createResponse.body.id; diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/get_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/get_notification_policy.ts index 0935164d1bd67..637518c7dd2e0 100644 --- a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/get_notification_policy.ts +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/get_notification_policy.ts @@ -38,7 +38,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'policy-name', description: 'policy-description', - workflow_id: 'policy-workflow-id', + destinations: [{ type: 'workflow', id: 'policy-workflow-id' }], + matcher: "env == 'production' && region == 'us-east-1'", + group_by: ['service.name'], + throttle: { interval: '10m' }, }); const createdPolicyId = createResponse.body.id; @@ -53,7 +56,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(response.body.version).to.be.a('string'); expect(response.body.name).to.be('policy-name'); expect(response.body.description).to.be('policy-description'); - expect(response.body.workflow_id).to.be('policy-workflow-id'); + expect(response.body.destinations).to.eql([{ type: 'workflow', id: 'policy-workflow-id' }]); + expect(response.body.matcher).to.be("env == 'production' && region == 'us-east-1'"); + expect(response.body.group_by).to.eql(['service.name']); + expect(response.body.throttle).to.eql({ interval: '10m' }); expect(response.body.createdAt).to.be.a('string'); expect(response.body.updatedAt).to.be.a('string'); }); diff --git a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/update_notification_policy.ts b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/update_notification_policy.ts index 549e38600952e..6d11c94b5f653 100644 --- a/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/update_notification_policy.ts +++ b/x-pack/platform/test/api_integration_deployment_agnostic/apis/alerting_v2/notification_policy/update_notification_policy.ts @@ -38,7 +38,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'original-policy', description: 'original-policy-description', - workflow_id: 'original-workflow-id', + destinations: [{ type: 'workflow', id: 'original-workflow-id' }], + matcher: "env == 'production' && region == 'us-east-1'", + group_by: ['service.name'], + throttle: { interval: '1m' }, }); expect(createResponse.status).to.be(200); @@ -52,8 +55,11 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .set(samlAuth.getInternalRequestHeader()) .send({ name: 'updated-policy', - workflow_id: 'updated-workflow-id', + destinations: [{ type: 'workflow', id: 'updated-workflow-id' }], description: 'updated-policy-description', + matcher: "env == 'production' && region == 'us-west-2'", + group_by: ['service.name', 'environment'], + throttle: { interval: '5m' }, version: currentVersion, }); @@ -61,8 +67,11 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(response.body.id).to.be(createdPolicyId); expect(response.body.version).to.be.a('string'); expect(response.body.name).to.be('updated-policy'); - expect(response.body.workflow_id).to.be('updated-workflow-id'); + expect(response.body.destinations).to.eql([{ type: 'workflow', id: 'updated-workflow-id' }]); expect(response.body.description).to.be('updated-policy-description'); + expect(response.body.matcher).to.be("env == 'production' && region == 'us-west-2'"); + expect(response.body.group_by).to.eql(['service.name', 'environment']); + expect(response.body.throttle).to.eql({ interval: '5m' }); expect(response.body.updatedAt).to.be.a('string'); }); @@ -74,7 +83,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'original-policy', description: 'original-policy-description', - workflow_id: 'original-workflow-id', + destinations: [{ type: 'workflow', id: 'original-workflow-id' }], + matcher: "env == 'production' && region == 'us-east-1'", + group_by: ['service.name'], + throttle: { interval: '1m' }, }); expect(createResponse.status).to.be(200); @@ -92,7 +104,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(response.body.version).to.be.a('string'); expect(response.body.name).to.be('only-name-updated'); expect(response.body.description).to.be('original-policy-description'); - expect(response.body.workflow_id).to.be('original-workflow-id'); + expect(response.body.destinations).to.eql([{ type: 'workflow', id: 'original-workflow-id' }]); + expect(response.body.matcher).to.be("env == 'production' && region == 'us-east-1'"); + expect(response.body.group_by).to.eql(['service.name']); + expect(response.body.throttle).to.eql({ interval: '1m' }); }); it('should update only description', async () => { @@ -103,7 +118,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'original-policy', description: 'original-policy-description', - workflow_id: 'original-workflow-id', + destinations: [{ type: 'workflow', id: 'original-workflow-id' }], + matcher: "env == 'production' && region == 'us-east-1'", + group_by: ['service.name'], + throttle: { interval: '1m' }, }); expect(createResponse.status).to.be(200); @@ -121,7 +139,50 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { expect(response.body.version).to.be.a('string'); expect(response.body.name).to.be('original-policy'); expect(response.body.description).to.be('only-description-updated'); - expect(response.body.workflow_id).to.be('original-workflow-id'); + expect(response.body.destinations).to.eql([{ type: 'workflow', id: 'original-workflow-id' }]); + expect(response.body.matcher).to.be("env == 'production' && region == 'us-east-1'"); + expect(response.body.group_by).to.eql(['service.name']); + expect(response.body.throttle).to.eql({ interval: '1m' }); + }); + + it('should update only matcher, group_by, and throttle', async () => { + const createResponse = await supertestWithoutAuth + .post(NOTIFICATION_POLICY_API_PATH) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + name: 'original-policy', + description: 'original-policy-description', + destinations: [{ type: 'workflow', id: 'original-workflow-id' }], + matcher: "env == 'production' && region == 'us-east-1'", + group_by: ['service.name'], + throttle: { interval: '1m' }, + }); + + expect(createResponse.status).to.be(200); + + const createdPolicyId = createResponse.body.id as string; + const currentVersion = createResponse.body.version as string; + + const response = await supertestWithoutAuth + .put(`${NOTIFICATION_POLICY_API_PATH}/${createdPolicyId}`) + .set(roleAuthc.apiKeyHeader) + .set(samlAuth.getInternalRequestHeader()) + .send({ + matcher: "env == 'staging' && region == 'eu-central-1'", + group_by: ['service.name', 'host.name'], + throttle: { interval: '15m' }, + version: currentVersion, + }); + + expect(response.status).to.be(200); + expect(response.body.version).to.be.a('string'); + expect(response.body.name).to.be('original-policy'); + expect(response.body.description).to.be('original-policy-description'); + expect(response.body.destinations).to.eql([{ type: 'workflow', id: 'original-workflow-id' }]); + expect(response.body.matcher).to.be("env == 'staging' && region == 'eu-central-1'"); + expect(response.body.group_by).to.eql(['service.name', 'host.name']); + expect(response.body.throttle).to.eql({ interval: '15m' }); }); it('should return 404 when updating a non-existent notification policy', async () => { @@ -132,7 +193,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .send({ name: 'some-name', description: 'some-description', - workflow_id: 'some-workflow-id', + destinations: [{ type: 'workflow', id: 'some-workflow-id' }], version: 'WzEsMV0=', });