diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.test.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.test.ts index 765f58a2ddd4b..da3196f25d75b 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.test.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.test.ts @@ -94,6 +94,9 @@ describe('RulesClient', () => { page: 1, per_page: 20, }); + mockSavedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [], + }); ensureRuleExecutorTaskScheduledMock.mockResolvedValue({ id: 'task-123' }); getRuleExecutorTaskIdMock.mockReturnValue('task:fallback'); @@ -400,6 +403,133 @@ describe('RulesClient', () => { }); }); + describe('getRules', () => { + it('returns rules for the provided ids', async () => { + const client = createClient(); + const so1Attrs = createRuleSoAttributes({ + metadata: { name: 'rule-get-many-1' }, + }); + const so2Attrs = createRuleSoAttributes({ + metadata: { name: 'rule-get-many-2' }, + }); + + mockSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: 'rule-id-get-many-1', + type: RULE_SAVED_OBJECT_TYPE, + attributes: so1Attrs, + references: [], + }, + { + id: 'rule-id-get-many-2', + type: RULE_SAVED_OBJECT_TYPE, + attributes: so2Attrs, + references: [], + }, + ], + }); + + const res = await client.getRules(['rule-id-get-many-1', 'rule-id-get-many-2']); + + expect(mockSavedObjectsClient.bulkGet).toHaveBeenCalledWith([ + { type: RULE_SAVED_OBJECT_TYPE, id: 'rule-id-get-many-1' }, + { type: RULE_SAVED_OBJECT_TYPE, id: 'rule-id-get-many-2' }, + ]); + expect(res).toHaveLength(2); + expect(res[0]).toEqual( + expect.objectContaining({ + id: 'rule-id-get-many-1', + metadata: expect.objectContaining({ name: 'rule-get-many-1' }), + }) + ); + expect(res[1]).toEqual( + expect.objectContaining({ + id: 'rule-id-get-many-2', + metadata: expect.objectContaining({ name: 'rule-get-many-2' }), + }) + ); + }); + + it('excludes missing ids returned as bulk get errors', async () => { + const client = createClient(); + const soAttrs = createRuleSoAttributes({ + metadata: { name: 'rule-get-many-success' }, + }); + + mockSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: 'rule-id-get-many-success', + type: RULE_SAVED_OBJECT_TYPE, + attributes: soAttrs, + references: [], + }, + { + id: 'rule-id-get-many-missing', + type: RULE_SAVED_OBJECT_TYPE, + attributes: {} as RuleSavedObjectAttributes, + references: [], + error: { + statusCode: 404, + error: 'Not Found', + message: 'Saved object [alerting-rule/rule-id-get-many-missing] not found', + }, + }, + ], + }); + + const res = await client.getRules(['rule-id-get-many-success', 'rule-id-get-many-missing']); + + expect(res).toHaveLength(1); + expect(res[0]).toEqual( + expect.objectContaining({ + id: 'rule-id-get-many-success', + metadata: expect.objectContaining({ name: 'rule-get-many-success' }), + }) + ); + }); + + it('ignores documents with non-404 errors and returns valid documents', async () => { + const client = createClient(); + const validAttrs = createRuleSoAttributes({ + metadata: { name: 'rule-get-many-valid' }, + }); + + mockSavedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + { + id: 'rule-id-get-many-valid', + type: RULE_SAVED_OBJECT_TYPE, + attributes: validAttrs, + references: [], + }, + { + id: 'rule-id-get-many-failure', + type: RULE_SAVED_OBJECT_TYPE, + attributes: {} as RuleSavedObjectAttributes, + references: [], + error: { + statusCode: 500, + error: 'Internal Server Error', + message: 'bulk get failed', + }, + }, + ], + }); + + const res = await client.getRules(['rule-id-get-many-valid', 'rule-id-get-many-failure']); + + expect(res).toHaveLength(1); + expect(res[0]).toEqual( + expect.objectContaining({ + id: 'rule-id-get-many-valid', + metadata: expect.objectContaining({ name: 'rule-get-many-valid' }), + }) + ); + }); + }); + describe('deleteRule', () => { it('removes the scheduled task and deletes the rule', async () => { const client = createClient(); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.ts index f46609de84ae2..c75392c558cb2 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/rules_client/rules_client.ts @@ -187,6 +187,18 @@ export class RulesClient { } } + public async getRules(ids: string[]): Promise { + const result = await this.rulesSavedObjectService.bulkGetByIds(ids); + + return result.flatMap((doc) => { + if ('error' in doc) { + return []; + } + + return [transformRuleSoAttributesToRuleApiResponse(doc.id, doc.attributes)]; + }); + } + public async deleteRule({ id }: { id: string }): Promise { const { spaceId } = this.getSpaceContext(); diff --git a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/rules_saved_object_service/rules_saved_object_service.ts b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/rules_saved_object_service/rules_saved_object_service.ts index ae56470d3f0b3..b15b09dc54b83 100644 --- a/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/rules_saved_object_service/rules_saved_object_service.ts +++ b/x-pack/platform/plugins/shared/alerting_v2/server/lib/services/rules_saved_object_service/rules_saved_object_service.ts @@ -12,17 +12,30 @@ 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 type { SavedObjectError } from '@kbn/core/types'; import { RULE_SAVED_OBJECT_TYPE } from '../../../saved_objects'; import type { RuleSavedObjectAttributes } from '../../../saved_objects'; import type { AlertingServerStartDependencies } from '../../../types'; import { spaceIdToNamespace } from '../../space_id_to_namespace'; +export type RulesSavedObjectsBulkGetResultItem = + | { + id: string; + attributes: RuleSavedObjectAttributes; + version?: string; + } + | { + id: string; + error: SavedObjectError; + }; + export interface RulesSavedObjectServiceContract { create(params: { attrs: RuleSavedObjectAttributes; id?: string }): Promise; get( id: string, spaceId?: string ): Promise<{ id: string; attributes: RuleSavedObjectAttributes; version?: string }>; + bulkGetByIds(ids: string[], spaceId?: string): Promise; update(params: { id: string; attrs: RuleSavedObjectAttributes; version?: string }): Promise; delete(params: { id: string }): Promise; find(params: { page: number; perPage: number }): Promise<{ @@ -72,6 +85,27 @@ export class RulesSavedObjectService implements RulesSavedObjectServiceContract return { id: doc.id, attributes: doc.attributes, version: doc.version }; } + public async bulkGetByIds( + ids: string[], + spaceId?: string + ): Promise { + const namespace = spaceIdToNamespace(this.spaces, spaceId); + if (ids.length === 0) { + return []; + } + + const result = await this.client.bulkGet( + ids.map((id) => ({ type: RULE_SAVED_OBJECT_TYPE, id }), namespace ? { namespace } : undefined) + ); + + return result.saved_objects.map((doc) => { + if ('error' in doc && doc.error) { + return { id: doc.id, error: doc.error }; + } + return { id: doc.id, attributes: doc.attributes, version: doc.version }; + }); + } + public async update({ id, attrs,