From 434d650b9f99ce4b9356ea2530b73782d6ba6dc8 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 10 Jun 2021 10:38:11 +0200 Subject: [PATCH 01/17] add `isExportable` SO export API --- src/core/server/index.ts | 1 + .../export/collect_exported_objects.test.ts | 256 +++++++++- .../export/collect_exported_objects.ts | 120 ++++- src/core/server/saved_objects/export/index.ts | 1 + .../export/saved_objects_exporter.test.ts | 436 +++++++++--------- .../export/saved_objects_exporter.ts | 18 +- src/core/server/saved_objects/export/types.ts | 14 + src/core/server/saved_objects/index.ts | 1 + src/core/server/saved_objects/types.ts | 45 +- .../public/lib/extract_export_details.ts | 6 + 10 files changed, 657 insertions(+), 241 deletions(-) diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 2e495657d3410..84693191896cc 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -295,6 +295,7 @@ export type { SavedObjectsCreatePointInTimeFinderOptions, SavedObjectsCreateOptions, SavedObjectsExportResultDetails, + SavedObjectsExportExcludedObject, SavedObjectsFindResult, SavedObjectsFindResponse, SavedObjectsImportConflictError, diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index 0929ff0d40910..d2d42f928b56d 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -10,8 +10,10 @@ import { applyExportTransformsMock } from './collect_exported_objects.test.mocks import { savedObjectsClientMock } from '../../mocks'; import { httpServerMock } from '../../http/http_server.mocks'; import { SavedObject, SavedObjectError } from '../../../types'; +import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; import { collectExportedObjects } from './collect_exported_objects'; +import { IsObjectExportablePredicate } from '../types'; const createObject = (parts: Partial): SavedObject => ({ id: 'id', @@ -33,8 +35,30 @@ const toIdTuple = (obj: SavedObject) => ({ type: obj.type, id: obj.id }); describe('collectExportedObjects', () => { let savedObjectsClient: ReturnType; let request: ReturnType; + let typeRegistry: SavedObjectTypeRegistry; + + const registerType = ( + name: string, + { + onExport, + isExportable, + }: { onExport?: SavedObjectsExportTransform; isExportable?: IsObjectExportablePredicate } = {} + ) => { + typeRegistry.registerType({ + name, + hidden: false, + namespaceType: 'single', + mappings: { properties: {} }, + management: { + importableAndExportable: true, + onExport, + isExportable, + }, + }); + }; beforeEach(() => { + typeRegistry = new SavedObjectTypeRegistry(); savedObjectsClient = savedObjectsClientMock.create(); request = httpServerMock.createKibanaRequest(); applyExportTransformsMock.mockImplementation(({ objects }) => objects); @@ -58,12 +82,13 @@ describe('collectExportedObjects', () => { }); const fooTransform: SavedObjectsExportTransform = jest.fn(); + registerType('foo', { onExport: fooTransform }); await collectExportedObjects({ objects: [obj1, obj2], savedObjectsClient, request, - exportTransforms: { foo: fooTransform }, + typeRegistry, includeReferences: true, }); @@ -75,6 +100,42 @@ describe('collectExportedObjects', () => { }); }); + it('calls `isExportable` with the correct parameters', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + const fooExportable: IsObjectExportablePredicate = jest.fn().mockReturnValue(true); + registerType('foo', { isExportable: fooExportable }); + + const barExportable: IsObjectExportablePredicate = jest.fn().mockReturnValue(true); + registerType('bar', { isExportable: barExportable }); + + await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + }); + + expect(fooExportable).toHaveBeenCalledTimes(2); + expect(fooExportable).toHaveBeenCalledWith(foo1); + expect(fooExportable).toHaveBeenCalledWith(foo2); + + expect(barExportable).toHaveBeenCalledTimes(1); + expect(barExportable).toHaveBeenCalledWith(bar3); + }); + it('returns the collected objects', async () => { const foo1 = createObject({ type: 'foo', @@ -96,6 +157,10 @@ describe('collectExportedObjects', () => { id: '3', }); + registerType('foo'); + registerType('bar'); + registerType('dolly'); + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, dolly3]); savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [bar2], @@ -105,7 +170,7 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -113,6 +178,117 @@ describe('collectExportedObjects', () => { expect(objects.map(toIdTuple)).toEqual([foo1, dolly3, bar2].map(toIdTuple)); }); + it('excludes objects filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + registerType('foo', { isExportable: (obj) => obj.id !== '2' }); + registerType('bar', { isExportable: () => true }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + }); + + expect(objects).toEqual([foo1, bar3]); + expect(excludedObjects).toEqual([foo2].map(toIdTuple)); + }); + + it('excludes references filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + references: [ + { + type: 'bar', + id: '2', + name: 'bar-2', + }, + { + type: 'excluded', + id: '1', + name: 'excluded-1', + }, + ], + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + }); + const excluded1 = createObject({ + type: 'excluded', + id: '1', + }); + + registerType('foo'); + registerType('bar'); + registerType('excluded', { isExportable: () => false }); + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [bar2, excluded1], + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + }); + + expect(objects).toEqual([foo1, bar2]); + expect(excludedObjects).toEqual([excluded1].map(toIdTuple)); + }); + + it('excludes additional objects filtered by the `isExportable` predicate', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + }); + const excluded1 = createObject({ + type: 'excluded', + id: '1', + }); + + registerType('foo'); + registerType('bar'); + registerType('excluded', { isExportable: () => false }); + + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [ + ...objects, + bar2, + excluded1, + ]); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + }); + + expect(objects).toEqual([foo1, bar2]); + expect(excludedObjects).toEqual([excluded1].map(toIdTuple)); + }); + it('returns the missing references', async () => { const foo1 = createObject({ type: 'foo', @@ -163,7 +339,7 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -185,7 +361,7 @@ describe('collectExportedObjects', () => { objects: [obj1, obj2], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -228,7 +404,7 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -302,7 +478,7 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -366,7 +542,7 @@ describe('collectExportedObjects', () => { objects: [foo1, bar2], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -411,7 +587,7 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -474,7 +650,7 @@ describe('collectExportedObjects', () => { objects: [foo1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: true, }); @@ -490,6 +666,66 @@ describe('collectExportedObjects', () => { expect.any(Object) ); }); + + it('excludes references filtered by the `isExportable` predicate for additional objects returned by the export transform', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const bar2 = createObject({ + type: 'bar', + id: '2', + references: [ + { + type: 'dolly', + id: '3', + name: 'dolly-3', + }, + { + type: 'baz', + id: '4', + name: 'baz-4', + }, + ], + }); + const dolly3 = createObject({ + type: 'dolly', + id: '3', + references: [ + { + type: 'baz', + id: '4', + name: 'baz-4', + }, + ], + }); + const baz4 = createObject({ + type: 'baz', + id: '4', + }); + + registerType('foo'); + registerType('bar'); + registerType('dolly'); + registerType('baz', { isExportable: () => false }); + + applyExportTransformsMock.mockImplementationOnce(({ objects }) => [...objects, bar2]); + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [dolly3, baz4], + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + }); + + expect(objects).toEqual([foo1, bar2, dolly3]); + expect(excludedObjects).toEqual([baz4].map(toIdTuple)); + }); }); describe('when `includeReferences` is `false`', () => { @@ -510,7 +746,7 @@ describe('collectExportedObjects', () => { objects: [obj1], savedObjectsClient, request, - exportTransforms: {}, + typeRegistry, includeReferences: false, }); diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index d45782a83c284..26bc719b5b26e 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -8,7 +8,8 @@ import type { SavedObject } from '../../../types'; import type { KibanaRequest } from '../../http'; -import { SavedObjectsClientContract } from '../types'; +import { SavedObjectsClientContract, IsObjectExportablePredicate } from '../types'; +import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; import { applyExportTransforms } from './apply_export_transforms'; @@ -22,41 +23,75 @@ interface CollectExportedObjectOptions { /** The http request initiating the export. */ request: KibanaRequest; /** export transform per type */ - exportTransforms: Record; + typeRegistry: ISavedObjectTypeRegistry; } interface CollectExportedObjectResult { objects: SavedObject[]; + excludedObjects: ExcludedObject[]; missingRefs: CollectedReference[]; } +interface ExcludedObject { + id: string; + type: string; + reason?: string; +} + export const collectExportedObjects = async ({ objects, includeReferences = true, namespace, request, - exportTransforms, + typeRegistry, savedObjectsClient, }: CollectExportedObjectOptions): Promise => { + const exportTransforms = buildTransforms(typeRegistry); + const isExportable = buildIsExportable(typeRegistry); + const collectedObjects: SavedObject[] = []; const collectedMissingRefs: CollectedReference[] = []; + const collectedNonExportableObjects: SavedObject[] = []; const alreadyProcessed: Set = new Set(); let currentObjects = objects; do { - const transformed = ( + currentObjects = currentObjects.filter((object) => !alreadyProcessed.has(objKey(object))); + + // first, evict current objects that are not exportable + const { + exportable: untransformedExportableInitialObjects, + nonExportable: nonExportableInitialObjects, + } = await splitByExportability(currentObjects, isExportable); + collectedNonExportableObjects.push(...nonExportableInitialObjects); + nonExportableInitialObjects.forEach((obj) => alreadyProcessed.add(objKey(obj))); + + // second, apply export transforms to exportable objects + const transformedObjects = ( await applyExportTransforms({ request, - objects: currentObjects, + objects: untransformedExportableInitialObjects, transforms: exportTransforms, }) ).filter((object) => !alreadyProcessed.has(objKey(object))); + transformedObjects.forEach((obj) => alreadyProcessed.add(objKey(obj))); - transformed.forEach((obj) => alreadyProcessed.add(objKey(obj))); - collectedObjects.push(...transformed); + // last, evict additional objects that are not exportable + const { included: exportableInitialObjects, excluded: additionalObjects } = splitByKeys( + transformedObjects, + untransformedExportableInitialObjects.map((obj) => objKey(obj)) + ); + const { + exportable: exportableAdditionalObjects, + nonExportable: nonExportableAdditionalObjects, + } = await splitByExportability(additionalObjects, isExportable); + const allExportableObjects = [...exportableInitialObjects, ...exportableAdditionalObjects]; + collectedNonExportableObjects.push(...nonExportableAdditionalObjects); + collectedObjects.push(...allExportableObjects); + // if `includeReferences` is true, recurse on exportable objects' references. if (includeReferences) { - const references = collectReferences(transformed, alreadyProcessed); + const references = collectReferences(allExportableObjects, alreadyProcessed); if (references.length) { const { objects: fetchedObjects, missingRefs } = await fetchReferences({ references, @@ -75,6 +110,10 @@ export const collectExportedObjects = async ({ return { objects: collectedObjects, + excludedObjects: collectedNonExportableObjects.map((obj) => ({ + type: obj.type, + id: obj.id, + })), missingRefs: collectedMissingRefs, }; }; @@ -126,3 +165,68 @@ const fetchReferences = async ({ .map((obj) => ({ type: obj.type, id: obj.id })), }; }; + +const buildTransforms = (typeRegistry: ISavedObjectTypeRegistry) => + typeRegistry.getAllTypes().reduce((transforms, type) => { + if (type.management?.onExport) { + return { + ...transforms, + [type.name]: type.management.onExport, + }; + } + return transforms; + }, {} as Record); + +const buildIsExportable = ( + typeRegistry: ISavedObjectTypeRegistry +): IsObjectExportablePredicate => { + const exportablePerType = typeRegistry.getAllTypes().reduce((transforms, type) => { + if (type.management?.isExportable) { + transforms[type.name] = type.management.isExportable; + } + return transforms; + }, {} as Record); + + return (obj: SavedObject) => { + const typePredicate = exportablePerType[obj.type]; + return typePredicate ? typePredicate(obj) : true; + }; +}; + +const splitByExportability = async ( + objects: SavedObject[], + isExportable: IsObjectExportablePredicate +) => { + const exportableObjects: SavedObject[] = []; + const nonExportableObjects: SavedObject[] = []; + await Promise.all( + objects.map(async (obj) => { + const exportable = await isExportable(obj); + if (exportable) { + exportableObjects.push(obj); + } else { + nonExportableObjects.push(obj); + } + }) + ); + return { + exportable: exportableObjects, + nonExportable: nonExportableObjects, + }; +}; + +const splitByKeys = (objects: SavedObject[], keys: ObjectKey[]) => { + const included: SavedObject[] = []; + const excluded: SavedObject[] = []; + objects.forEach((obj) => { + if (keys.includes(objKey(obj))) { + included.push(obj); + } else { + excluded.push(obj); + } + }); + return { + included, + excluded, + }; +}; diff --git a/src/core/server/saved_objects/export/index.ts b/src/core/server/saved_objects/export/index.ts index 4af184e54b49c..d9b48ce431117 100644 --- a/src/core/server/saved_objects/export/index.ts +++ b/src/core/server/saved_objects/export/index.ts @@ -13,6 +13,7 @@ export type { SavedObjectsExportResultDetails, SavedObjectsExportTransformContext, SavedObjectsExportTransform, + SavedObjectsExportExcludedObject, } from './types'; export { SavedObjectsExporter } from './saved_objects_exporter'; export type { ISavedObjectsExporter } from './saved_objects_exporter'; diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts index 6bdb8003de49d..5968c8dabe8a8 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.test.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.test.ts @@ -77,32 +77,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -185,6 +187,8 @@ describe('getSortedObjectsForExport()', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(1); expect(response[response.length - 1]).toMatchInlineSnapshot(` Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, "exportedCount": 20, "missingRefCount": 0, "missingReferences": Array [], @@ -269,6 +273,8 @@ describe('getSortedObjectsForExport()', () => { expect(savedObjectsClient.find).toHaveBeenCalledTimes(2); expect(response[response.length - 1]).toMatchInlineSnapshot(` Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, "exportedCount": 1500, "missingRefCount": 0, "missingReferences": Array [], @@ -422,32 +428,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -579,32 +587,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -674,26 +684,28 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 1, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 1, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -770,32 +782,34 @@ describe('getSortedObjectsForExport()', () => { const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.find).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -929,38 +943,40 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object { - "name": "foo", - }, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "bar", - }, - "id": "2", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object { - "name": "baz", - }, - "id": "3", - "references": Array [], - "type": "index-pattern", - }, - Object { - "exportedCount": 3, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object { + "name": "foo", + }, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "bar", + }, + "id": "2", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object { + "name": "baz", + }, + "id": "3", + "references": Array [], + "type": "index-pattern", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 3, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); }); }); @@ -1003,32 +1019,34 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ @@ -1211,32 +1229,34 @@ describe('getSortedObjectsForExport()', () => { }); const response = await readStreamToCompletion(exportStream); expect(response).toMatchInlineSnapshot(` - Array [ - Object { - "attributes": Object {}, - "id": "1", - "references": Array [], - "type": "index-pattern", - }, - Object { - "attributes": Object {}, - "id": "2", - "references": Array [ - Object { - "id": "1", - "name": "name", - "type": "index-pattern", - }, - ], - "type": "search", - }, - Object { - "exportedCount": 2, - "missingRefCount": 0, - "missingReferences": Array [], - }, - ] - `); + Array [ + Object { + "attributes": Object {}, + "id": "1", + "references": Array [], + "type": "index-pattern", + }, + Object { + "attributes": Object {}, + "id": "2", + "references": Array [ + Object { + "id": "1", + "name": "name", + "type": "index-pattern", + }, + ], + "type": "search", + }, + Object { + "excludedObjects": Array [], + "excludedObjectsCount": 0, + "exportedCount": 2, + "missingRefCount": 0, + "missingReferences": Array [], + }, + ] + `); expect(savedObjectsClient.bulkGet).toMatchInlineSnapshot(` [MockFunction] { "calls": Array [ diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index 9d56bb4872a6d..c170e1842904c 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -18,7 +18,6 @@ import { SavedObjectExportBaseOptions, SavedObjectsExportByObjectOptions, SavedObjectsExportByTypeOptions, - SavedObjectsExportTransform, } from './types'; import { SavedObjectsExportError } from './errors'; import { collectExportedObjects } from './collect_exported_objects'; @@ -34,8 +33,8 @@ export type ISavedObjectsExporter = PublicMethodsOf; */ export class SavedObjectsExporter { readonly #savedObjectsClient: SavedObjectsClientContract; - readonly #exportTransforms: Record; readonly #exportSizeLimit: number; + readonly #typeRegistry: ISavedObjectTypeRegistry; readonly #log: Logger; constructor({ @@ -52,15 +51,7 @@ export class SavedObjectsExporter { this.#log = logger; this.#savedObjectsClient = savedObjectsClient; this.#exportSizeLimit = exportSizeLimit; - this.#exportTransforms = typeRegistry.getAllTypes().reduce((transforms, type) => { - if (type.management?.onExport) { - return { - ...transforms, - [type.name]: type.management.onExport, - }; - } - return transforms; - }, {} as Record); + this.#typeRegistry = typeRegistry; } /** @@ -121,12 +112,13 @@ export class SavedObjectsExporter { const { objects: collectedObjects, missingRefs: missingReferences, + excludedObjects, } = await collectExportedObjects({ objects: savedObjects, includeReferences: includeReferencesDeep, namespace, request, - exportTransforms: this.#exportTransforms, + typeRegistry: this.#typeRegistry, savedObjectsClient: this.#savedObjectsClient, }); @@ -142,6 +134,8 @@ export class SavedObjectsExporter { exportedCount: exportedObjects.length, missingRefCount: missingReferences.length, missingReferences, + excludedObjectsCount: excludedObjects.length, + excludedObjects, }; this.#log.debug(`Exporting [${redactedObjects.length}] saved objects.`); return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts index 7891af6df5b1b..c6c346c880fa2 100644 --- a/src/core/server/saved_objects/export/types.ts +++ b/src/core/server/saved_objects/export/types.ts @@ -72,6 +72,20 @@ export interface SavedObjectsExportResultDetails { /** the missing reference type. */ type: string; }>; + /** number of objects that were excluded from the export */ + excludedObjectsCount: number; + /** excluded objects details */ + excludedObjects: SavedObjectsExportExcludedObject[]; +} + +/** @public */ +export interface SavedObjectsExportExcludedObject { + /** id of the excluded object */ + id: string; + /** type of the excluded object */ + type: string; + /** optional cause of the exclusion */ + reason?: string; } /** diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index 2af25e0cdef3f..ee1fcacd0f784 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -41,6 +41,7 @@ export type { SavedObjectsExportError, SavedObjectsExportTransformContext, SavedObjectsExportTransform, + SavedObjectsExportExcludedObject, } from './export'; export { SavedObjectsSerializer } from './serialization'; diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index d3bfdcc6923dc..bd0b1cffcd88c 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -253,7 +253,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat * * @public */ -export interface SavedObjectsType { +export interface SavedObjectsType { /** * The name of the type, which is also used as the internal id. */ @@ -337,7 +337,7 @@ export interface SavedObjectsType { /** * An optional {@link SavedObjectsTypeManagementDefinition | saved objects management section} definition for the type. */ - management?: SavedObjectsTypeManagementDefinition; + management?: SavedObjectsTypeManagementDefinition; } /** @@ -345,7 +345,7 @@ export interface SavedObjectsType { * * @public */ -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { /** * Is the type importable or exportable. Defaults to `false`. */ @@ -432,4 +432,43 @@ export interface SavedObjectsTypeManagementDefinition { * @remarks `importableAndExportable` must be `true` to specify this property. */ onImport?: SavedObjectsImportHook; + + /** + * Allow to specify exportability on the object's granularity level. + * + * @example + * Registering a type with per-object exportability + * ```ts + * // src/plugins/my_plugin/server/plugin.ts + * import { myType } from './saved_objects'; + * + * export class Plugin() { + * setup: (core: CoreSetup) => { + * core.savedObjects.registerType({ + * ...myType, + * management: { + * ...myType.management, + * isExportable: (object) => { + * if (object.attributes.myCustomAttr === 'foo') { + * return false; + * } + * return true; + * } + * }, + * }); + * } + * } + * ``` + * + * @remarks an exportable predicate should be deterministic + * @remarks this is only used when `importableAndExportable` is true + */ + isExportable?: IsObjectExportablePredicate; } + +/** + * @public + */ +export type IsObjectExportablePredicate = ( + obj: SavedObject +) => boolean | Promise; diff --git a/src/plugins/saved_objects_management/public/lib/extract_export_details.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.ts index 40f8039a8cdae..4d142330dca88 100644 --- a/src/plugins/saved_objects_management/public/lib/extract_export_details.ts +++ b/src/plugins/saved_objects_management/public/lib/extract_export_details.ts @@ -33,6 +33,12 @@ export interface SavedObjectsExportResultDetails { id: string; type: string; }>; + excludedObjectsCount: number; + excludedObjects: Array<{ + id: string; + type: string; + reason?: string; + }>; } function isExportDetails(object: any): object is SavedObjectsExportResultDetails { From 8ddcceee1d1fb92455f9f1a715f999e1ef3cdb63 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 10 Jun 2021 11:05:41 +0200 Subject: [PATCH 02/17] add warning when export contains excluded objects --- .../public/lib/extract_export_details.test.ts | 58 +++++++++++++++++-- .../saved_objects_table.test.tsx | 51 +++++++++++++++- .../objects_table/saved_objects_table.tsx | 56 +++++++++++------- 3 files changed, 137 insertions(+), 28 deletions(-) diff --git a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts index c9f9b8591860d..64375f71953df 100644 --- a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts +++ b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts @@ -14,14 +14,22 @@ describe('extractExportDetails', () => { }; const detailsLine = ( exported: number, - missingRefs: SavedObjectsExportResultDetails['missingReferences'] = [] + { + missingRefs = [], + excludedObjects = [], + }: { + missingRefs?: SavedObjectsExportResultDetails['missingReferences']; + excludedObjects?: SavedObjectsExportResultDetails['excludedObjects']; + } = {} ) => { return ( JSON.stringify({ exportedCount: exported, missingRefCount: missingRefs.length, missingReferences: missingRefs, - }) + '\n' + excludedObjectsCount: excludedObjects.length, + excludedObjects, + } as SavedObjectsExportResultDetails) + '\n' ); }; @@ -43,6 +51,8 @@ describe('extractExportDetails', () => { exportedCount: 3, missingRefCount: 0, missingReferences: [], + excludedObjectsCount: 0, + excludedObjects: [], }); }); @@ -51,10 +61,12 @@ describe('extractExportDetails', () => { [ [ objLine('1', 'index-pattern'), - detailsLine(1, [ - { id: '2', type: 'index-pattern' }, - { id: '3', type: 'index-pattern' }, - ]), + detailsLine(1, { + missingRefs: [ + { id: '2', type: 'index-pattern' }, + { id: '3', type: 'index-pattern' }, + ], + }), ].join(''), ], { @@ -71,6 +83,40 @@ describe('extractExportDetails', () => { { id: '2', type: 'index-pattern' }, { id: '3', type: 'index-pattern' }, ], + excludedObjectsCount: 0, + excludedObjects: [], + }); + }); + + it('should properly extract the excluded objects', async () => { + const exportData = new Blob( + [ + [ + objLine('1', 'index-pattern'), + detailsLine(1, { + excludedObjects: [ + { id: '2', type: 'index-pattern', reason: 'foo' }, + { id: '3', type: 'index-pattern' }, + ], + }), + ].join(''), + ], + { + type: 'application/ndjson', + endings: 'transparent', + } + ); + const result = await extractExportDetails(exportData); + expect(result).not.toBeUndefined(); + expect(result).toEqual({ + exportedCount: 1, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 2, + excludedObjects: [ + { id: '2', type: 'index-pattern', reason: 'foo' }, + { id: '3', type: 'index-pattern' }, + ], }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 364b3ab0d9eb6..cff00b042075c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -258,7 +258,7 @@ describe('SavedObjectsTable', () => { }); }); - it('should display a warning is export contains missing references', async () => { + it('should display a warning if the export contains missing references', async () => { const mockSelectedSavedObjects = [ { id: '1', type: 'index-pattern' }, { id: '3', type: 'dashboard' }, @@ -280,6 +280,8 @@ describe('SavedObjectsTable', () => { exportedCount: 2, missingRefCount: 1, missingReferences: [{ id: '7', type: 'visualisation' }], + excludedObjectsCount: 0, + excludedObjects: [], })); const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); @@ -303,6 +305,53 @@ describe('SavedObjectsTable', () => { }); }); + it('should display a warning if the export contains excluded objects', async () => { + const mockSelectedSavedObjects = [ + { id: '1', type: 'index-pattern' }, + { id: '3', type: 'dashboard' }, + ] as SavedObjectWithMetadata[]; + + const mockSavedObjects = mockSelectedSavedObjects.map((obj) => ({ + _id: obj.id, + _source: {}, + })); + + const mockSavedObjectsClient = { + ...defaultProps.savedObjectsClient, + bulkGet: jest.fn().mockImplementation(() => ({ + savedObjects: mockSavedObjects, + })), + }; + + extractExportDetailsMock.mockImplementation(() => ({ + exportedCount: 2, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 1, + excludedObjects: [{ id: '7', type: 'visualisation' }], + })); + + const component = shallowRender({ savedObjectsClient: mockSavedObjectsClient }); + + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + + // Set some as selected + component.instance().onSelectionChanged(mockSelectedSavedObjects); + + await component.instance().onExport(true); + + expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); + expect(notifications.toasts.addWarning).toHaveBeenCalledWith({ + title: + 'Your file is downloading in the background. ' + + 'Some objects were excluded from the export. ' + + 'Please see the last line in the exported file for a list of excluded objects.', + }); + }); + it('should allow the user to choose when exporting all', async () => { const component = shallowRender(); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index c207766918a70..f3bf5962c031e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -358,7 +358,7 @@ export class SavedObjectsTable extends Component { @@ -395,31 +395,45 @@ export class SavedObjectsTable extends Component { + showExportCompleteMessage = (exportDetails: SavedObjectsExportResultDetails | undefined) => { const { notifications } = this.props; - if (exportDetails && exportDetails.missingReferences.length > 0) { - notifications.toasts.addWarning({ - title: i18n.translate( - 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', - { - defaultMessage: - 'Your file is downloading in the background. ' + - 'Some related objects could not be found. ' + - 'Please see the last line in the exported file for a list of missing objects.', - } - ), - }); - } else { - notifications.toasts.addSuccess({ - title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { - defaultMessage: 'Your file is downloading in the background', - }), - }); + if (exportDetails) { + if (exportDetails.missingReferences.length > 0) { + return notifications.toasts.addWarning({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.export.successWithMissingRefsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some related objects could not be found. ' + + 'Please see the last line in the exported file for a list of missing objects.', + } + ), + }); + } + if (exportDetails.excludedObjects.length > 0) { + return notifications.toasts.addWarning({ + title: i18n.translate( + 'savedObjectsManagement.objectsTable.export.successWithExcludedObjectsNotification', + { + defaultMessage: + 'Your file is downloading in the background. ' + + 'Some objects were excluded from the export. ' + + 'Please see the last line in the exported file for a list of excluded objects.', + } + ), + }); + } } + return notifications.toasts.addSuccess({ + title: i18n.translate('savedObjectsManagement.objectsTable.export.successNotification', { + defaultMessage: 'Your file is downloading in the background', + }), + }); }; finishImport = () => { From d5762d64f2f8d8e03816abdfb983811961df1766 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 10 Jun 2021 13:34:08 +0200 Subject: [PATCH 03/17] add FTR test --- .../export/collect_exported_objects.test.ts | 8 +- .../export/collect_exported_objects.ts | 8 +- src/core/server/saved_objects/export/types.ts | 2 +- .../saved_objects/saved_objects_service.ts | 2 +- src/core/server/saved_objects/types.ts | 20 +- .../export_exclusion/data.json | 116 ++++ .../export_exclusion/mappings.json | 505 ++++++++++++++++++ .../server/plugin.ts | 21 + .../export_transform.ts | 79 ++- 9 files changed, 741 insertions(+), 20 deletions(-) create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json create mode 100644 test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index d2d42f928b56d..f71cf9b1f5725 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -13,7 +13,7 @@ import { SavedObject, SavedObjectError } from '../../../types'; import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; import { collectExportedObjects } from './collect_exported_objects'; -import { IsObjectExportablePredicate } from '../types'; +import { SavedObjectsExportablePredicate } from '../types'; const createObject = (parts: Partial): SavedObject => ({ id: 'id', @@ -42,7 +42,7 @@ describe('collectExportedObjects', () => { { onExport, isExportable, - }: { onExport?: SavedObjectsExportTransform; isExportable?: IsObjectExportablePredicate } = {} + }: { onExport?: SavedObjectsExportTransform; isExportable?: SavedObjectsExportablePredicate } = {} ) => { typeRegistry.registerType({ name, @@ -114,10 +114,10 @@ describe('collectExportedObjects', () => { id: '3', }); - const fooExportable: IsObjectExportablePredicate = jest.fn().mockReturnValue(true); + const fooExportable: SavedObjectsExportablePredicate = jest.fn().mockReturnValue(true); registerType('foo', { isExportable: fooExportable }); - const barExportable: IsObjectExportablePredicate = jest.fn().mockReturnValue(true); + const barExportable: SavedObjectsExportablePredicate = jest.fn().mockReturnValue(true); registerType('bar', { isExportable: barExportable }); await collectExportedObjects({ diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index 26bc719b5b26e..6cc6ab65773ab 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -8,7 +8,7 @@ import type { SavedObject } from '../../../types'; import type { KibanaRequest } from '../../http'; -import { SavedObjectsClientContract, IsObjectExportablePredicate } from '../types'; +import { SavedObjectsClientContract, SavedObjectsExportablePredicate } from '../types'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; import { applyExportTransforms } from './apply_export_transforms'; @@ -179,13 +179,13 @@ const buildTransforms = (typeRegistry: ISavedObjectTypeRegistry) => const buildIsExportable = ( typeRegistry: ISavedObjectTypeRegistry -): IsObjectExportablePredicate => { +): SavedObjectsExportablePredicate => { const exportablePerType = typeRegistry.getAllTypes().reduce((transforms, type) => { if (type.management?.isExportable) { transforms[type.name] = type.management.isExportable; } return transforms; - }, {} as Record); + }, {} as Record); return (obj: SavedObject) => { const typePredicate = exportablePerType[obj.type]; @@ -195,7 +195,7 @@ const buildIsExportable = ( const splitByExportability = async ( objects: SavedObject[], - isExportable: IsObjectExportablePredicate + isExportable: SavedObjectsExportablePredicate ) => { const exportableObjects: SavedObject[] = []; const nonExportableObjects: SavedObject[] = []; diff --git a/src/core/server/saved_objects/export/types.ts b/src/core/server/saved_objects/export/types.ts index c6c346c880fa2..a805ec3a06c1b 100644 --- a/src/core/server/saved_objects/export/types.ts +++ b/src/core/server/saved_objects/export/types.ts @@ -172,7 +172,7 @@ export interface SavedObjectsExportTransformContext { * * @public */ -export type SavedObjectsExportTransform = ( +export type SavedObjectsExportTransform = ( context: SavedObjectsExportTransformContext, objects: Array> ) => SavedObject[] | Promise; diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index b95f187cd44ca..e50c1e540bfaf 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -141,7 +141,7 @@ export interface SavedObjectsServiceSetup { * } * ``` */ - registerType: (type: SavedObjectsType) => void; + registerType: (type: SavedObjectsType) => void; } /** diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index bd0b1cffcd88c..ed98c94ae2e34 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -253,7 +253,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat * * @public */ -export interface SavedObjectsType { +export interface SavedObjectsType { /** * The name of the type, which is also used as the internal id. */ @@ -345,7 +345,7 @@ export interface SavedObjectsType { * * @public */ -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { /** * Is the type importable or exportable. Defaults to `false`. */ @@ -363,12 +363,12 @@ export interface SavedObjectsTypeManagementDefinition { * Function returning the title to display in the management table. * If not defined, will use the object's type and id to generate a label. */ - getTitle?: (savedObject: SavedObject) => string; + getTitle?: (savedObject: SavedObject) => string; /** * Function returning the url to use to redirect to the editing page of this object. * If not defined, editing will not be allowed. */ - getEditUrl?: (savedObject: SavedObject) => string; + getEditUrl?: (savedObject: SavedObject) => string; /** * Function returning the url to use to redirect to this object from the management section. * If not defined, redirecting to the object will not be allowed. @@ -377,7 +377,9 @@ export interface SavedObjectsTypeManagementDefinition { * the object page, relative to the base path. `uiCapabilitiesPath` is the path to check in the * {@link Capabilities | uiCapabilities} to check if the user has permission to access the object. */ - getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; + getInAppUrl?: ( + savedObject: SavedObject + ) => { path: string; uiCapabilitiesPath: string }; /** * An optional export transform function that can be used transform the objects of the registered type during * the export process. @@ -388,7 +390,7 @@ export interface SavedObjectsTypeManagementDefinition { * * @remarks `importableAndExportable` must be `true` to specify this property. */ - onExport?: SavedObjectsExportTransform; + onExport?: SavedObjectsExportTransform; /** * An optional {@link SavedObjectsImportHook | import hook} to use when importing given type. * @@ -431,7 +433,7 @@ export interface SavedObjectsTypeManagementDefinition { * @remarks messages returned in the warnings are user facing and must be translated. * @remarks `importableAndExportable` must be `true` to specify this property. */ - onImport?: SavedObjectsImportHook; + onImport?: SavedObjectsImportHook; /** * Allow to specify exportability on the object's granularity level. @@ -463,12 +465,12 @@ export interface SavedObjectsTypeManagementDefinition { * @remarks an exportable predicate should be deterministic * @remarks this is only used when `importableAndExportable` is true */ - isExportable?: IsObjectExportablePredicate; + isExportable?: SavedObjectsExportablePredicate; } /** * @public */ -export type IsObjectExportablePredicate = ( +export type SavedObjectsExportablePredicate = ( obj: SavedObject ) => boolean | Promise; diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json new file mode 100644 index 0000000000000..c8978c72acf49 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json @@ -0,0 +1,116 @@ +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:1", + "source": { + "test-is-exportable": { + "title": "obj 1", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [ + { + "type": "test-is-exportable", + "id": "2", + "name": "ref-1" + }, + { + "type": "test-is-exportable", + "id": "3", + "name": "ref-2" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:2", + "source": { + "test-is-exportable": { + "title": "obj 2", + "enabled": false + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:3", + "source": { + "test-is-exportable": { + "title": "obj 3", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [ + { + "type": "test-is-exportable", + "id": "4", + "name": "ref-1" + }, + { + "type": "test-is-exportable", + "id": "5", + "name": "ref-2" + } + ] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:4", + "source": { + "test-is-exportable": { + "title": "obj 4", + "enabled": false + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:5", + "source": { + "test-is-exportable": { + "title": "obj 5", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json new file mode 100644 index 0000000000000..abec2eeb77492 --- /dev/null +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/mappings.json @@ -0,0 +1,505 @@ +{ + "type": "index", + "value": { + "index": ".kibana", + "settings": { + "index": { + "number_of_shards": "1", + "auto_expand_replicas": "0-1", + "number_of_replicas": "0" + } + }, + "mappings": { + "dynamic": "strict", + "properties": { + "test-export-transform": { + "properties": { + "title": { "type": "text" }, + "enabled": { "type": "boolean" } + } + }, + "test-is-exportable": { + "properties": { + "title": { "type": "text" }, + "enabled": { "type": "boolean" } + } + }, + "test-export-add": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-add-dep": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-transform-error": { + "properties": { + "title": { "type": "text" } + } + }, + "test-export-invalid-transform": { + "properties": { + "title": { "type": "text" } + } + }, + "apm-telemetry": { + "properties": { + "has_any_services": { + "type": "boolean" + }, + "services_per_agent": { + "properties": { + "go": { + "type": "long", + "null_value": 0 + }, + "java": { + "type": "long", + "null_value": 0 + }, + "js-base": { + "type": "long", + "null_value": 0 + }, + "nodejs": { + "type": "long", + "null_value": 0 + }, + "python": { + "type": "long", + "null_value": 0 + }, + "ruby": { + "type": "long", + "null_value": 0 + } + } + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "id": { + "type": "text", + "index": false + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword" + } + } + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "accessibility:disableAnimations": { + "type": "boolean" + }, + "buildNum": { + "type": "keyword" + }, + "dateFormat:tz": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "defaultIndex": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "telemetry:optIn": { + "type": "boolean" + } + } + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "optionsJSON": { + "type": "text" + }, + "panelsJSON": { + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "type": "keyword" + }, + "pause": { + "type": "boolean" + }, + "section": { + "type": "integer" + }, + "value": { + "type": "integer" + } + } + }, + "timeFrom": { + "type": "keyword" + }, + "timeRestore": { + "type": "boolean" + }, + "timeTo": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "map": { + "properties": { + "bounds": { + "dynamic": false, + "properties": {} + }, + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "properties": { + "fieldFormatMap": { + "type": "text" + }, + "fields": { + "type": "text" + }, + "intervalName": { + "type": "keyword" + }, + "notExpandable": { + "type": "boolean" + }, + "sourceFilters": { + "type": "text" + }, + "timeFieldName": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "typeMeta": { + "type": "keyword" + } + } + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "index-pattern": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "space": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + } + } + }, + "namespace": { + "type": "keyword" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "server": { + "properties": { + "uuid": { + "type": "keyword" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "initials": { + "type": "keyword" + }, + "name": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "spaceId": { + "type": "keyword" + }, + "telemetry": { + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 2048 + } + } + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "savedSearchId": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + } + } + } + } +} diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts index 408ac03dd946b..d1d7c0be016c3 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts +++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts @@ -152,6 +152,27 @@ export class SavedObjectExportTransformsPlugin implements Plugin { getTitle: (obj) => obj.attributes.title, }, }); + + // example of a SO type implementing the `isExportable` API + savedObjects.registerType<{ enabled: boolean; title: string }>({ + name: 'test-is-exportable', + hidden: false, + namespaceType: 'single', + mappings: { + properties: { + title: { type: 'text' }, + enabled: { type: 'boolean' }, + }, + }, + management: { + defaultSearchField: 'title', + importableAndExportable: true, + getTitle: (obj) => obj.attributes.title, + isExportable: (obj) => { + return obj.attributes.enabled === true; + }, + }, + }); } public start() {} diff --git a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts index 2b845cb6327b8..abc37991c7d1b 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts @@ -8,6 +8,7 @@ import expect from '@kbn/expect'; import type { SavedObject } from '../../../../src/core/types'; +import type { SavedObjectsExportResultDetails } from '../../../../src/core/server'; import { PluginFunctionalProviderContext } from '../../services'; function parseNdJson(input: string): Array> { @@ -139,7 +140,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { }); }); - describe('FOO nested export transforms', () => { + describe('nested export transforms', () => { before(async () => { await esArchiver.load( '../functional/fixtures/es_archiver/saved_objects_management/nested_export_transform' @@ -183,5 +184,81 @@ export default function ({ getService }: PluginFunctionalProviderContext) { }); }); }); + + describe('isExportable API', () => { + before(async () => { + await esArchiver.load( + '../functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + }); + + after(async () => { + await esArchiver.unload( + '../functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + ); + }); + + it('should only export objects returning `true` for `isExportable`', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: true, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text).sort((obj1, obj2) => + obj1.id.localeCompare(obj2.id) + ); + expect(objects.map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:1', + 'test-is-exportable:3', + 'test-is-exportable:5', + ]); + }); + }); + + it('lists objects that got filtered', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '1', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + const exportDetails = (objects[ + objects.length - 1 + ] as unknown) as SavedObjectsExportResultDetails; + + expect(exportDetails.excludedObjectsCount).to.eql(2); + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: '2', + }, + { + type: 'test-is-exportable', + id: '4', + }, + ]); + }); + }); + }); }); } From b2a92fec24b3059d4f5efb0789e3894ab7027310 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 10 Jun 2021 13:36:10 +0200 Subject: [PATCH 04/17] fix API integration assertions --- .../common/suites/export.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index 94b75f1fd536d..83189293ded92 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -21,12 +21,15 @@ const { export interface ExportTestDefinition extends TestDefinition { request: ReturnType; } + export type ExportTestSuite = TestSuite; + interface SuccessResult { type: string; id: string; originId?: string; } + export interface ExportTestCase { title: string; type: string; @@ -135,7 +138,13 @@ export const createRequest = ({ type, id }: ExportTestCase) => const getTestTitle = ({ failure, title }: ExportTestCase) => `${failure?.reason || 'success'} ["${title}"]`; -const EMPTY_RESULT = { exportedCount: 0, missingRefCount: 0, missingReferences: [] }; +const EMPTY_RESULT = { + exportedCount: 0, + missingRefCount: 0, + missingReferences: [], + excludedObjectsCount: 0, + excludedObjects: [], +}; export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectSavedObjectForbiddenBulkGet = expectResponses.forbiddenTypes('bulk_get'); @@ -189,6 +198,8 @@ export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest Date: Thu, 10 Jun 2021 15:31:33 +0200 Subject: [PATCH 05/17] lint --- .../saved_objects/export/collect_exported_objects.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index f71cf9b1f5725..d8cb09b540596 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -42,7 +42,10 @@ describe('collectExportedObjects', () => { { onExport, isExportable, - }: { onExport?: SavedObjectsExportTransform; isExportable?: SavedObjectsExportablePredicate } = {} + }: { + onExport?: SavedObjectsExportTransform; + isExportable?: SavedObjectsExportablePredicate; + } = {} ) => { typeRegistry.registerType({ name, From 0e269f579c5049fe284ed97deae79c5f65df71e5 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 10 Jun 2021 15:46:16 +0200 Subject: [PATCH 06/17] fix assertions again --- .../test/saved_object_api_integration/common/suites/export.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/test/saved_object_api_integration/common/suites/export.ts b/x-pack/test/saved_object_api_integration/common/suites/export.ts index 83189293ded92..9766d764cada4 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/export.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/export.ts @@ -139,11 +139,11 @@ const getTestTitle = ({ failure, title }: ExportTestCase) => `${failure?.reason || 'success'} ["${title}"]`; const EMPTY_RESULT = { + excludedObjects: [], + excludedObjectsCount: 0, exportedCount: 0, missingRefCount: 0, missingReferences: [], - excludedObjectsCount: 0, - excludedObjects: [], }; export function exportTestSuiteFactory(esArchiver: any, supertest: SuperTest) { From 1ea546bce24e06d4205b200b7fc5b8c05bb0dd17 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 10 Jun 2021 16:39:52 +0200 Subject: [PATCH 07/17] doc --- src/core/server/saved_objects/types.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index ed98c94ae2e34..ef77fab94aaca 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -436,7 +436,16 @@ export interface SavedObjectsTypeManagementDefinition { onImport?: SavedObjectsImportHook; /** - * Allow to specify exportability on the object's granularity level. + * Allow to specify exportability with an object granularity. + * + * If specified, `isExportable` will be called during export for each + * of this type's objects, and the ones not matching the predicate will + * be evicted from the export. + * + * When implementing both `isExportable` and `onExport`, it is mandatory that + * `isExportable` returns the same value for an object before and after going + * though the export transform. + * E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` * * @example * Registering a type with per-object exportability @@ -462,7 +471,6 @@ export interface SavedObjectsTypeManagementDefinition { * } * ``` * - * @remarks an exportable predicate should be deterministic * @remarks this is only used when `importableAndExportable` is true */ isExportable?: SavedObjectsExportablePredicate; From 6a8b06a1bf6ddebdbec5259164ffdbfa7eb71b04 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 10 Jun 2021 20:40:47 +0200 Subject: [PATCH 08/17] update generated doc --- .../core/server/kibana-plugin-core-server.md | 1 + ...ver.savedobjectsexportexcludedobject.id.md | 13 +++++ ...server.savedobjectsexportexcludedobject.md | 21 ++++++++ ...savedobjectsexportexcludedobject.reason.md | 13 +++++ ...r.savedobjectsexportexcludedobject.type.md | 13 +++++ ...ectsexportresultdetails.excludedobjects.md | 13 +++++ ...xportresultdetails.excludedobjectscount.md | 13 +++++ ...-server.savedobjectsexportresultdetails.md | 2 + ...core-server.savedobjectsexporttransform.md | 2 +- ...in-core-server.savedobjectsservicesetup.md | 2 +- ...r.savedobjectsservicesetup.registertype.md | 2 +- ...core-server.savedobjectstype.management.md | 2 +- ...ana-plugin-core-server.savedobjectstype.md | 4 +- ...ectstypemanagementdefinition.getediturl.md | 2 +- ...ctstypemanagementdefinition.getinappurl.md | 2 +- ...bjectstypemanagementdefinition.gettitle.md | 2 +- ...tstypemanagementdefinition.isexportable.md | 49 +++++++++++++++++++ ...er.savedobjectstypemanagementdefinition.md | 13 ++--- ...bjectstypemanagementdefinition.onexport.md | 2 +- ...bjectstypemanagementdefinition.onimport.md | 2 +- ...ver.savedobjecttyperegistry.getalltypes.md | 4 +- ...egistry.getimportableandexportabletypes.md | 4 +- ...-server.savedobjecttyperegistry.gettype.md | 4 +- ...savedobjecttyperegistry.getvisibletypes.md | 4 +- src/core/server/server.api.md | 39 +++++++++------ 25 files changed, 189 insertions(+), 39 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 32a2f3312708c..50d491117565e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -167,6 +167,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsDeleteOptions](./kibana-plugin-core-server.savedobjectsdeleteoptions.md) | | | [SavedObjectsExportByObjectOptions](./kibana-plugin-core-server.savedobjectsexportbyobjectoptions.md) | Options for the [export by objects API](./kibana-plugin-core-server.savedobjectsexporter.exportbyobjects.md) | | [SavedObjectsExportByTypeOptions](./kibana-plugin-core-server.savedobjectsexportbytypeoptions.md) | Options for the [export by type API](./kibana-plugin-core-server.savedobjectsexporter.exportbytypes.md) | +| [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) | | | [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) | Structure of the export result details entry | | [SavedObjectsExportTransformContext](./kibana-plugin-core-server.savedobjectsexporttransformcontext.md) | Context passed down to a [export transform function](./kibana-plugin-core-server.savedobjectsexporttransform.md) | | [SavedObjectsFindOptions](./kibana-plugin-core-server.savedobjectsfindoptions.md) | | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md new file mode 100644 index 0000000000000..f7b96e71c8e53 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) + +## SavedObjectsExportExcludedObject.id property + +id of the excluded object + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md new file mode 100644 index 0000000000000..4766ae25a936d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) + +## SavedObjectsExportExcludedObject interface + + +Signature: + +```typescript +export interface SavedObjectsExportExcludedObject +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [id](./kibana-plugin-core-server.savedobjectsexportexcludedobject.id.md) | string | id of the excluded object | +| [reason](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) | string | optional cause of the exclusion | +| [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) | string | type of the excluded object | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md new file mode 100644 index 0000000000000..0adb1ba35e696 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [reason](./kibana-plugin-core-server.savedobjectsexportexcludedobject.reason.md) + +## SavedObjectsExportExcludedObject.reason property + +optional cause of the exclusion + +Signature: + +```typescript +reason?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md new file mode 100644 index 0000000000000..be28ac2d0ffb6 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportExcludedObject](./kibana-plugin-core-server.savedobjectsexportexcludedobject.md) > [type](./kibana-plugin-core-server.savedobjectsexportexcludedobject.type.md) + +## SavedObjectsExportExcludedObject.type property + +type of the excluded object + +Signature: + +```typescript +type: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md new file mode 100644 index 0000000000000..90432bf6d6705 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) > [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) + +## SavedObjectsExportResultDetails.excludedObjects property + +excluded objects details + +Signature: + +```typescript +excludedObjects: SavedObjectsExportExcludedObject[]; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md new file mode 100644 index 0000000000000..05846e28b9cab --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsExportResultDetails](./kibana-plugin-core-server.savedobjectsexportresultdetails.md) > [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) + +## SavedObjectsExportResultDetails.excludedObjectsCount property + +number of objects that were excluded from the export + +Signature: + +```typescript +excludedObjectsCount: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md index d98088c5f45be..f017f2329170b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexportresultdetails.md @@ -16,6 +16,8 @@ export interface SavedObjectsExportResultDetails | Property | Type | Description | | --- | --- | --- | +| [excludedObjects](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjects.md) | SavedObjectsExportExcludedObject[] | excluded objects details | +| [excludedObjectsCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.excludedobjectscount.md) | number | number of objects that were excluded from the export | | [exportedCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.exportedcount.md) | number | number of successfully exported objects | | [missingRefCount](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingrefcount.md) | number | number of missing references | | [missingReferences](./kibana-plugin-core-server.savedobjectsexportresultdetails.missingreferences.md) | Array<{
id: string;
type: string;
}> | missing references details | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md index 50d4c5425e8fd..2effed1ae9d70 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsexporttransform.md @@ -11,7 +11,7 @@ A type's export transform function will be executed once per user-initiated expo Signature: ```typescript -export declare type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; +export declare type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md index 56ebb48707f59..a1bc99ce8d13d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md @@ -52,6 +52,6 @@ export class Plugin() { | Property | Type | Description | | --- | --- | --- | | [addClientWrapper](./kibana-plugin-core-server.savedobjectsservicesetup.addclientwrapper.md) | (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void | Add a [client wrapper factory](./kibana-plugin-core-server.savedobjectsclientwrapperfactory.md) with the given priority. | -| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | (type: SavedObjectsType) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | +| [registerType](./kibana-plugin-core-server.savedobjectsservicesetup.registertype.md) | <Attributes = any>(type: SavedObjectsType<Attributes>) => void | Register a [savedObjects type](./kibana-plugin-core-server.savedobjectstype.md) definition.See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) and [migration format](./kibana-plugin-core-server.savedobjectmigrationmap.md) for more details about these. | | [setClientFactoryProvider](./kibana-plugin-core-server.savedobjectsservicesetup.setclientfactoryprovider.md) | (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void | Set the default [factory provider](./kibana-plugin-core-server.savedobjectsclientfactoryprovider.md) for creating Saved Objects clients. Only one provider can be set, subsequent calls to this method will fail. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md index 54e01d3110a2d..7f74ce4d7bea7 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md @@ -11,7 +11,7 @@ See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdef Signature: ```typescript -registerType: (type: SavedObjectsType) => void; +registerType: (type: SavedObjectsType) => void; ``` ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md index fbaf58f959075..d98c553656b1f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.management.md @@ -9,5 +9,5 @@ An optional [saved objects management section](./kibana-plugin-core-server.saved Signature: ```typescript -management?: SavedObjectsTypeManagementDefinition; +management?: SavedObjectsTypeManagementDefinition; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md index d882938d731c8..c3aba5261561f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstype.md @@ -7,7 +7,7 @@ Signature: ```typescript -export interface SavedObjectsType +export interface SavedObjectsType ``` ## Remarks @@ -54,7 +54,7 @@ Example after converting to a multi-namespace (shareable) type in 8.1: Note: migration function(s) can be optionally specified for any of these versions and will not interfere with the conversion process. | | [hidden](./kibana-plugin-core-server.savedobjectstype.hidden.md) | boolean | Is the type hidden by default. If true, repositories will not have access to this type unless explicitly declared as an extraType when creating the repository.See [createInternalRepository](./kibana-plugin-core-server.savedobjectsservicestart.createinternalrepository.md). | | [indexPattern](./kibana-plugin-core-server.savedobjectstype.indexpattern.md) | string | If defined, the type instances will be stored in the given index instead of the default one. | -| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | +| [management](./kibana-plugin-core-server.savedobjectstype.management.md) | SavedObjectsTypeManagementDefinition<Attributes> | An optional [saved objects management section](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) definition for the type. | | [mappings](./kibana-plugin-core-server.savedobjectstype.mappings.md) | SavedObjectsTypeMappingDefinition | The [mapping definition](./kibana-plugin-core-server.savedobjectstypemappingdefinition.md) for the type. | | [migrations](./kibana-plugin-core-server.savedobjectstype.migrations.md) | SavedObjectMigrationMap | (() => SavedObjectMigrationMap) | An optional map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) or a function returning a map of [migrations](./kibana-plugin-core-server.savedobjectmigrationfn.md) to be used to migrate the type. | | [name](./kibana-plugin-core-server.savedobjectstype.name.md) | string | The name of the type, which is also used as the internal id. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md index f5488d8f0310d..75f820d7a8e56 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md @@ -9,5 +9,5 @@ Function returning the url to use to redirect to the editing page of this object Signature: ```typescript -getEditUrl?: (savedObject: SavedObject) => string; +getEditUrl?: (savedObject: SavedObject) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md index 7b31dda402571..d6d50840aaadb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md @@ -9,7 +9,7 @@ Function returning the url to use to redirect to this object from the management Signature: ```typescript -getInAppUrl?: (savedObject: SavedObject) => { +getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string; }; diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md index 2f39acc66f451..75784666ef963 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md @@ -9,5 +9,5 @@ Function returning the title to display in the management table. If not defined, Signature: ```typescript -getTitle?: (savedObject: SavedObject) => string; +getTitle?: (savedObject: SavedObject) => string; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md new file mode 100644 index 0000000000000..225296bcfa2c2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsTypeManagementDefinition](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.md) > [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) + +## SavedObjectsTypeManagementDefinition.isExportable property + +Allow to specify exportability with an object granularity. + +If specified, `isExportable` will be called during export for each of this type's objects, and the ones not matching the predicate will be evicted from the export. + +When implementing both `isExportable` and `onExport`, it is mandatory that `isExportable` returns the same value for an object before and after going though the export transform. E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + +Signature: + +```typescript +isExportable?: SavedObjectsExportablePredicate; +``` + +## Remarks + +this is only used when `importableAndExportable` is true + +## Example + +Registering a type with per-object exportability + +```ts +// src/plugins/my_plugin/server/plugin.ts +import { myType } from './saved_objects'; + +export class Plugin() { + setup: (core: CoreSetup) => { + core.savedObjects.registerType({ + ...myType, + management: { + ...myType.management, + isExportable: (object) => { + if (object.attributes.myCustomAttr === 'foo') { + return false; + } + return true; + } + }, + }); + } +} + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md index e9cc2b12108d6..f49cfdaf387ae 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md @@ -9,7 +9,7 @@ Configuration options for the [type](./kibana-plugin-core-server.savedobjectstyp Signature: ```typescript -export interface SavedObjectsTypeManagementDefinition +export interface SavedObjectsTypeManagementDefinition ``` ## Properties @@ -17,11 +17,12 @@ export interface SavedObjectsTypeManagementDefinition | Property | Type | Description | | --- | --- | --- | | [defaultSearchField](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.defaultsearchfield.md) | string | The default search field to use for this type. Defaults to id. | -| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<any>) => string | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | -| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<any>) => {
path: string;
uiCapabilitiesPath: string;
} | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | -| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<any>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | +| [getEditUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getediturl.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the url to use to redirect to the editing page of this object. If not defined, editing will not be allowed. | +| [getInAppUrl](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.getinappurl.md) | (savedObject: SavedObject<Attributes>) => {
path: string;
uiCapabilitiesPath: string;
} | Function returning the url to use to redirect to this object from the management section. If not defined, redirecting to the object will not be allowed. | +| [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | | [icon](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.icon.md) | string | The eui icon name to display in the management table. If not defined, the default icon will be used. | | [importableAndExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | boolean | Is the type importable or exportable. Defaults to false. | -| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. | -| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | +| [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) | SavedObjectsExportablePredicate<Attributes> | Allow to specify exportability with an object granularity.If specified, isExportable will be called during export for each of this type's objects, and the ones not matching the predicate will be evicted from the export.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform<Attributes> | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. | +| [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook<Attributes> | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md index 6302b36a73c68..dee45d58474d8 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md @@ -13,7 +13,7 @@ See [the transform type documentation](./kibana-plugin-core-server.savedobjectse Signature: ```typescript -onExport?: SavedObjectsExportTransform; +onExport?: SavedObjectsExportTransform; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md index f6634c01c66ba..332247b8eb8e1 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md @@ -11,7 +11,7 @@ Import hooks are executed during the savedObjects import process and allow to in Signature: ```typescript -onImport?: SavedObjectsImportHook; +onImport?: SavedObjectsImportHook; ``` ## Remarks diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md index c839dd16d9a47..20d631ff74aca 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getalltypes.md @@ -11,9 +11,9 @@ To only get the visible types (which is the most common use case), use `getVisib Signature: ```typescript -getAllTypes(): SavedObjectsType[]; +getAllTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md index ab8a79c3a8455..1e29e632a6ec3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getimportableandexportabletypes.md @@ -9,9 +9,9 @@ Return all [types](./kibana-plugin-core-server.savedobjectstype.md) currently re Signature: ```typescript -getImportableAndExportableTypes(): SavedObjectsType[]; +getImportableAndExportableTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md index cfa52882bb89d..160aadb73cced 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.gettype.md @@ -9,7 +9,7 @@ Return the [type](./kibana-plugin-core-server.savedobjectstype.md) definition fo Signature: ```typescript -getType(type: string): SavedObjectsType | undefined; +getType(type: string): SavedObjectsType | undefined; ``` ## Parameters @@ -20,5 +20,5 @@ getType(type: string): SavedObjectsType | undefined; Returns: -`SavedObjectsType | undefined` +`SavedObjectsType | undefined` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md index a773c6a0a674f..05f22dcf7010b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjecttyperegistry.getvisibletypes.md @@ -11,9 +11,9 @@ A visible type is a type that doesn't explicitly define `hidden=true` during reg Signature: ```typescript -getVisibleTypes(): SavedObjectsType[]; +getVisibleTypes(): SavedObjectsType[]; ``` Returns: -`SavedObjectsType[]` +`SavedObjectsType[]` diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 379e4147ae024..c9e1c40fc8a8e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2535,8 +2535,17 @@ export class SavedObjectsExportError extends Error { readonly type: string; } +// @public (undocumented) +export interface SavedObjectsExportExcludedObject { + id: string; + reason?: string; + type: string; +} + // @public export interface SavedObjectsExportResultDetails { + excludedObjects: SavedObjectsExportExcludedObject[]; + excludedObjectsCount: number; exportedCount: number; missingRefCount: number; missingReferences: Array<{ @@ -2546,7 +2555,7 @@ export interface SavedObjectsExportResultDetails { } // @public -export type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; +export type SavedObjectsExportTransform = (context: SavedObjectsExportTransformContext, objects: Array>) => SavedObject[] | Promise; // @public export interface SavedObjectsExportTransformContext { @@ -2955,7 +2964,7 @@ export class SavedObjectsSerializer { // @public export interface SavedObjectsServiceSetup { addClientWrapper: (priority: number, id: string, factory: SavedObjectsClientWrapperFactory) => void; - registerType: (type: SavedObjectsType) => void; + registerType: (type: SavedObjectsType) => void; setClientFactoryProvider: (clientFactoryProvider: SavedObjectsClientFactoryProvider) => void; } @@ -2981,12 +2990,12 @@ export interface SavedObjectStatusMeta { } // @public (undocumented) -export interface SavedObjectsType { +export interface SavedObjectsType { convertToAliasScript?: string; convertToMultiNamespaceTypeVersion?: string; hidden: boolean; indexPattern?: string; - management?: SavedObjectsTypeManagementDefinition; + management?: SavedObjectsTypeManagementDefinition; mappings: SavedObjectsTypeMappingDefinition; migrations?: SavedObjectMigrationMap | (() => SavedObjectMigrationMap); name: string; @@ -2994,18 +3003,20 @@ export interface SavedObjectsType { } // @public -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { defaultSearchField?: string; - getEditUrl?: (savedObject: SavedObject) => string; - getInAppUrl?: (savedObject: SavedObject) => { + getEditUrl?: (savedObject: SavedObject) => string; + getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string; }; - getTitle?: (savedObject: SavedObject) => string; + getTitle?: (savedObject: SavedObject) => string; icon?: string; importableAndExportable?: boolean; - onExport?: SavedObjectsExportTransform; - onImport?: SavedObjectsImportHook; + // Warning: (ae-forgotten-export) The symbol "SavedObjectsExportablePredicate" needs to be exported by the entry point index.d.ts + isExportable?: SavedObjectsExportablePredicate; + onExport?: SavedObjectsExportTransform; + onImport?: SavedObjectsImportHook; } // @public @@ -3070,11 +3081,11 @@ export class SavedObjectsUtils { // @public export class SavedObjectTypeRegistry { - getAllTypes(): SavedObjectsType[]; - getImportableAndExportableTypes(): SavedObjectsType[]; + getAllTypes(): SavedObjectsType[]; + getImportableAndExportableTypes(): SavedObjectsType[]; getIndex(type: string): string | undefined; - getType(type: string): SavedObjectsType | undefined; - getVisibleTypes(): SavedObjectsType[]; + getType(type: string): SavedObjectsType | undefined; + getVisibleTypes(): SavedObjectsType[]; isHidden(type: string): boolean; isImportableAndExportable(type: string): boolean; isMultiNamespace(type: string): boolean; From d2a3d98a4ac6119bcbb9f0d70f21bc3ef8c3e3e9 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Mon, 14 Jun 2021 08:06:49 +0200 Subject: [PATCH 09/17] fix esarchiver paths --- .../test_suites/saved_objects_management/export_transform.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts index 201582a03235b..bceb0a102ebd3 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts @@ -188,13 +188,13 @@ export default function ({ getService }: PluginFunctionalProviderContext) { describe('isExportable API', () => { before(async () => { await esArchiver.load( - '../functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' ); }); after(async () => { await esArchiver.unload( - '../functional/fixtures/es_archiver/saved_objects_management/export_exclusion' + 'test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion' ); }); From 8bd90b462788741eb7945b4f44167c1bb9a16a1e Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 15 Jun 2021 16:24:40 +0200 Subject: [PATCH 10/17] use maps instead of objects --- .../export/apply_export_transforms.test.ts | 46 ++++++++++--------- .../export/apply_export_transforms.ts | 4 +- .../export/collect_exported_objects.ts | 21 ++++----- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/core/server/saved_objects/export/apply_export_transforms.test.ts b/src/core/server/saved_objects/export/apply_export_transforms.test.ts index 95c6bd80a1ac3..ed428ef5759a8 100644 --- a/src/core/server/saved_objects/export/apply_export_transforms.test.ts +++ b/src/core/server/saved_objects/export/apply_export_transforms.test.ts @@ -27,6 +27,8 @@ const createTransform = ( implementation: SavedObjectsExportTransform = (ctx, objs) => objs ): jest.MockedFunction => jest.fn(implementation); +const toMap = (record: Record): Map => new Map(Object.entries(record)); + const expectedContext = { request: expect.any(KibanaRequest), }; @@ -49,10 +51,10 @@ describe('applyExportTransforms', () => { await applyExportTransforms({ request, objects: [foo1, bar1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(fooTransform).toHaveBeenCalledTimes(1); @@ -71,10 +73,10 @@ describe('applyExportTransforms', () => { await applyExportTransforms({ request, objects: [foo1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(fooTransform).toHaveBeenCalledTimes(1); @@ -100,10 +102,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(result).toEqual([foo1, foo2, dolly1, bar1, hello1]); @@ -123,9 +125,9 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, foo2, bar1, bar2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }); expect(result).toEqual([foo1, foo2, dolly1, bar1, bar2]); @@ -150,9 +152,9 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }); expect(result).toEqual([foo1, foo2].map(disableFoo)); @@ -175,10 +177,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), }); expect(result).toEqual([foo1, dolly1, bar1, hello1]); @@ -201,10 +203,10 @@ describe('applyExportTransforms', () => { const result = await applyExportTransforms({ request, objects: [foo1, bar1], - transforms: { + transforms: toMap({ foo: fooTransform, bar: barTransform, - }, + }), sortFunction: (obj1, obj2) => (obj1.id > obj2.id ? 1 : -1), }); @@ -223,9 +225,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -247,9 +249,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -271,9 +273,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1, foo2], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Invalid transform performed on objects to export"` @@ -291,9 +293,9 @@ describe('applyExportTransforms', () => { applyExportTransforms({ request, objects: [foo1], - transforms: { + transforms: toMap({ foo: fooTransform, - }, + }), }) ).rejects.toThrowErrorMatchingInlineSnapshot(`"Error transforming objects to export"`); }); diff --git a/src/core/server/saved_objects/export/apply_export_transforms.ts b/src/core/server/saved_objects/export/apply_export_transforms.ts index 2a788a32b92f6..78e1dd7d6c117 100644 --- a/src/core/server/saved_objects/export/apply_export_transforms.ts +++ b/src/core/server/saved_objects/export/apply_export_transforms.ts @@ -15,7 +15,7 @@ import { getObjKey, SavedObjectComparator } from './utils'; interface ApplyExportTransformsOptions { objects: SavedObject[]; request: KibanaRequest; - transforms: Record; + transforms: Map; sortFunction?: SavedObjectComparator; } @@ -30,7 +30,7 @@ export const applyExportTransforms = async ({ let finalObjects: SavedObject[] = []; for (const [type, typeObjs] of Object.entries(byType)) { - const typeTransformFn = transforms[type]; + const typeTransformFn = transforms.get(type); if (typeTransformFn) { finalObjects = [ ...finalObjects, diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index 6cc6ab65773ab..09fb805a8631d 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -167,28 +167,25 @@ const fetchReferences = async ({ }; const buildTransforms = (typeRegistry: ISavedObjectTypeRegistry) => - typeRegistry.getAllTypes().reduce((transforms, type) => { + typeRegistry.getAllTypes().reduce((transformMap, type) => { if (type.management?.onExport) { - return { - ...transforms, - [type.name]: type.management.onExport, - }; + transformMap.set(type.name, type.management.onExport); } - return transforms; - }, {} as Record); + return transformMap; + }, new Map()); const buildIsExportable = ( typeRegistry: ISavedObjectTypeRegistry ): SavedObjectsExportablePredicate => { - const exportablePerType = typeRegistry.getAllTypes().reduce((transforms, type) => { + const exportablePerType = typeRegistry.getAllTypes().reduce((exportableMap, type) => { if (type.management?.isExportable) { - transforms[type.name] = type.management.isExportable; + exportableMap.set(type.name, type.management.isExportable); } - return transforms; - }, {} as Record); + return exportableMap; + }, new Map()); return (obj: SavedObject) => { - const typePredicate = exportablePerType[obj.type]; + const typePredicate = exportablePerType.get(obj.type); return typePredicate ? typePredicate(obj) : true; }; }; From 6e853511cf33def14c70384df8309f2369f87f41 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 15 Jun 2021 16:39:57 +0200 Subject: [PATCH 11/17] SavedObjectsExportablePredicate is no longer async --- .../export/collect_exported_objects.test.ts | 8 ++++--- .../export/collect_exported_objects.ts | 22 +++++++++---------- src/core/server/saved_objects/types.ts | 6 ++--- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index d8cb09b540596..34960991078fb 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -32,6 +32,8 @@ const createError = (parts: Partial = {}): SavedObjectError => const toIdTuple = (obj: SavedObject) => ({ type: obj.type, id: obj.id }); +const toMap = (record: Record): Map => new Map(Object.entries(record)); + describe('collectExportedObjects', () => { let savedObjectsClient: ReturnType; let request: ReturnType; @@ -98,7 +100,7 @@ describe('collectExportedObjects', () => { expect(applyExportTransformsMock).toHaveBeenCalledTimes(1); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [obj1, obj2], - transforms: { foo: fooTransform }, + transforms: toMap({ foo: fooTransform }), request, }); }); @@ -420,12 +422,12 @@ describe('collectExportedObjects', () => { expect(applyExportTransformsMock).toHaveBeenCalledTimes(2); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [foo1], - transforms: {}, + transforms: toMap({}), request, }); expect(applyExportTransformsMock).toHaveBeenCalledWith({ objects: [bar2], - transforms: {}, + transforms: toMap({}), request, }); }); diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index 09fb805a8631d..ba60ff1f43589 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -190,22 +190,22 @@ const buildIsExportable = ( }; }; -const splitByExportability = async ( +const splitByExportability = ( objects: SavedObject[], isExportable: SavedObjectsExportablePredicate ) => { const exportableObjects: SavedObject[] = []; const nonExportableObjects: SavedObject[] = []; - await Promise.all( - objects.map(async (obj) => { - const exportable = await isExportable(obj); - if (exportable) { - exportableObjects.push(obj); - } else { - nonExportableObjects.push(obj); - } - }) - ); + + objects.forEach((obj) => { + const exportable = isExportable(obj); + if (exportable) { + exportableObjects.push(obj); + } else { + nonExportableObjects.push(obj); + } + }); + return { exportable: exportableObjects, nonExportable: nonExportableObjects, diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 061e8e8e593d8..7b85eda5db3f2 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -253,7 +253,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat * * @public */ -export interface SavedObjectsType { +export interface SavedObjectsType { /** * The name of the type, which is also used as the internal id. */ @@ -345,7 +345,7 @@ export interface SavedObjectsType { * * @public */ -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { /** * Is the type importable or exportable. Defaults to `false`. */ @@ -481,4 +481,4 @@ export interface SavedObjectsTypeManagementDefinition { */ export type SavedObjectsExportablePredicate = ( obj: SavedObject -) => boolean | Promise; +) => boolean; From 08cd885ff7fe7d4562a3937edfc48f1ef5564a8c Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 15 Jun 2021 16:59:35 +0200 Subject: [PATCH 12/17] more docs --- src/core/server/saved_objects/types.ts | 19 ++++++++++++------- .../public/lib/extract_export_details.test.ts | 1 - 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 7b85eda5db3f2..1bb214de701e2 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -253,7 +253,7 @@ export type SavedObjectsNamespaceType = 'single' | 'multiple' | 'multiple-isolat * * @public */ -export interface SavedObjectsType { +export interface SavedObjectsType { /** * The name of the type, which is also used as the internal id. */ @@ -345,7 +345,7 @@ export interface SavedObjectsType { * * @public */ -export interface SavedObjectsTypeManagementDefinition { +export interface SavedObjectsTypeManagementDefinition { /** * Is the type importable or exportable. Defaults to `false`. */ @@ -388,6 +388,11 @@ export interface SavedObjectsTypeManagementDefinition { * * See {@link SavedObjectsExportTransform | the transform type documentation} for more info and examples. * + * When implementing both `isExportable` and `onExport`, it is mandatory that + * `isExportable` returns the same value for an object before and after going + * though the export transform. + * E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + * * @remarks `importableAndExportable` must be `true` to specify this property. */ onExport?: SavedObjectsExportTransform; @@ -436,11 +441,11 @@ export interface SavedObjectsTypeManagementDefinition { onImport?: SavedObjectsImportHook; /** - * Allow to specify exportability with an object granularity. + * Optional hook to specify whether an object should be exportable. * * If specified, `isExportable` will be called during export for each - * of this type's objects, and the ones not matching the predicate will - * be evicted from the export. + * of this type's objects in the export, and the ones not matching the + * predicate will be excluded from the export. * * When implementing both `isExportable` and `onExport`, it is mandatory that * `isExportable` returns the same value for an object before and after going @@ -448,7 +453,7 @@ export interface SavedObjectsTypeManagementDefinition { * E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` * * @example - * Registering a type with per-object exportability + * Registering a type with a per-object exportability predicate * ```ts * // src/plugins/my_plugin/server/plugin.ts * import { myType } from './saved_objects'; @@ -471,7 +476,7 @@ export interface SavedObjectsTypeManagementDefinition { * } * ``` * - * @remarks this is only used when `importableAndExportable` is true + * @remarks `importableAndExportable` must be `true` to specify this property. */ isExportable?: SavedObjectsExportablePredicate; } diff --git a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts index 64375f71953df..7487e57da9e8c 100644 --- a/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts +++ b/src/plugins/saved_objects_management/public/lib/extract_export_details.test.ts @@ -107,7 +107,6 @@ describe('extractExportDetails', () => { } ); const result = await extractExportDetails(exportData); - expect(result).not.toBeUndefined(); expect(result).toEqual({ exportedCount: 1, missingRefCount: 0, From 16ac1694f8439e1e5d7d99444834999e41b6e2bc Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 15 Jun 2021 19:53:13 +0200 Subject: [PATCH 13/17] generated doc --- ...r.savedobjectstypemanagementdefinition.isexportable.md | 8 ++++---- ...in-core-server.savedobjectstypemanagementdefinition.md | 4 ++-- ...erver.savedobjectstypemanagementdefinition.onexport.md | 2 ++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md index 225296bcfa2c2..fef178e1d9847 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md @@ -4,9 +4,9 @@ ## SavedObjectsTypeManagementDefinition.isExportable property -Allow to specify exportability with an object granularity. +Optional hook to specify whether an object should be exportable. -If specified, `isExportable` will be called during export for each of this type's objects, and the ones not matching the predicate will be evicted from the export. +If specified, `isExportable` will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export. When implementing both `isExportable` and `onExport`, it is mandatory that `isExportable` returns the same value for an object before and after going though the export transform. E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` @@ -18,11 +18,11 @@ isExportable?: SavedObjectsExportablePredicate; ## Remarks -this is only used when `importableAndExportable` is true +`importableAndExportable` must be `true` to specify this property. ## Example -Registering a type with per-object exportability +Registering a type with a per-object exportability predicate ```ts // src/plugins/my_plugin/server/plugin.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md index f49cfdaf387ae..8c42884eb0b31 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.md @@ -22,7 +22,7 @@ export interface SavedObjectsTypeManagementDefinition | [getTitle](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.gettitle.md) | (savedObject: SavedObject<Attributes>) => string | Function returning the title to display in the management table. If not defined, will use the object's type and id to generate a label. | | [icon](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.icon.md) | string | The eui icon name to display in the management table. If not defined, the default icon will be used. | | [importableAndExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.importableandexportable.md) | boolean | Is the type importable or exportable. Defaults to false. | -| [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) | SavedObjectsExportablePredicate<Attributes> | Allow to specify exportability with an object granularity.If specified, isExportable will be called during export for each of this type's objects, and the ones not matching the predicate will be evicted from the export.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | -| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform<Attributes> | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. | +| [isExportable](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.isexportable.md) | SavedObjectsExportablePredicate<Attributes> | Optional hook to specify whether an object should be exportable.If specified, isExportable will be called during export for each of this type's objects in the export, and the ones not matching the predicate will be excluded from the export.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | +| [onExport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md) | SavedObjectsExportTransform<Attributes> | An optional export transform function that can be used transform the objects of the registered type during the export process.It can be used to either mutate the exported objects, or add additional objects (of any type) to the export list.See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples.When implementing both isExportable and onExport, it is mandatory that isExportable returns the same value for an object before and after going though the export transform. E.g isExportable(objectBeforeTransform) === isExportable(objectAfterTransform) | | [onImport](./kibana-plugin-core-server.savedobjectstypemanagementdefinition.onimport.md) | SavedObjectsImportHook<Attributes> | An optional [import hook](./kibana-plugin-core-server.savedobjectsimporthook.md) to use when importing given type.Import hooks are executed during the savedObjects import process and allow to interact with the imported objects. See the [hook documentation](./kibana-plugin-core-server.savedobjectsimporthook.md) for more info. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md index dee45d58474d8..a0d41d2d64967 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectstypemanagementdefinition.onexport.md @@ -10,6 +10,8 @@ It can be used to either mutate the exported objects, or add additional objects See [the transform type documentation](./kibana-plugin-core-server.savedobjectsexporttransform.md) for more info and examples. +When implementing both `isExportable` and `onExport`, it is mandatory that `isExportable` returns the same value for an object before and after going though the export transform. E.g `isExportable(objectBeforeTransform) === isExportable(objectAfterTransform)` + Signature: ```typescript From 55f03c2f1b8d1f771429b22c2f5e53612740461b Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 15 Jun 2021 19:55:23 +0200 Subject: [PATCH 14/17] use info instead of warning when export contains excluded objects --- .../objects_table/saved_objects_table.test.tsx | 4 ++-- .../management_section/objects_table/saved_objects_table.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index cff00b042075c..9b8474fc08bbd 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -305,7 +305,7 @@ describe('SavedObjectsTable', () => { }); }); - it('should display a warning if the export contains excluded objects', async () => { + it('should display a specific message if the export contains excluded objects', async () => { const mockSelectedSavedObjects = [ { id: '1', type: 'index-pattern' }, { id: '3', type: 'dashboard' }, @@ -344,7 +344,7 @@ describe('SavedObjectsTable', () => { await component.instance().onExport(true); expect(fetchExportObjectsMock).toHaveBeenCalledWith(http, mockSelectedSavedObjects, true); - expect(notifications.toasts.addWarning).toHaveBeenCalledWith({ + expect(notifications.toasts.addSuccess).toHaveBeenCalledWith({ title: 'Your file is downloading in the background. ' + 'Some objects were excluded from the export. ' + diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index f3bf5962c031e..9f3962bcebb0e 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -416,7 +416,7 @@ export class SavedObjectsTable extends Component 0) { - return notifications.toasts.addWarning({ + return notifications.toasts.addSuccess({ title: i18n.translate( 'savedObjectsManagement.objectsTable.export.successWithExcludedObjectsNotification', { From a8ad0b6a6c19b125965bdd2cd76ad7e6224ce515 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Tue, 15 Jun 2021 21:08:51 +0200 Subject: [PATCH 15/17] try/catch on isExportable call and add exclusion reason --- .../export/collect_exported_objects.test.ts | 53 +++++++++++++++++-- .../export/collect_exported_objects.ts | 35 +++++++----- 2 files changed, 71 insertions(+), 17 deletions(-) diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index 34960991078fb..0fbe018ae5d80 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -12,7 +12,7 @@ import { httpServerMock } from '../../http/http_server.mocks'; import { SavedObject, SavedObjectError } from '../../../types'; import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; -import { collectExportedObjects } from './collect_exported_objects'; +import { collectExportedObjects, ExclusionReason } from './collect_exported_objects'; import { SavedObjectsExportablePredicate } from '../types'; const createObject = (parts: Partial): SavedObject => ({ @@ -31,6 +31,11 @@ const createError = (parts: Partial = {}): SavedObjectError => }); const toIdTuple = (obj: SavedObject) => ({ type: obj.type, id: obj.id }); +const toExcludedObject = (obj: SavedObject, reason: ExclusionReason = 'excluded') => ({ + type: obj.type, + id: obj.id, + reason, +}); const toMap = (record: Record): Map => new Map(Object.entries(record)); @@ -209,7 +214,45 @@ describe('collectExportedObjects', () => { }); expect(objects).toEqual([foo1, bar3]); - expect(excludedObjects).toEqual([foo2].map(toIdTuple)); + expect(excludedObjects).toEqual([foo2].map((obj) => toExcludedObject(obj))); + }); + + it('excludes objects when the predicate throws', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const bar3 = createObject({ + type: 'bar', + id: '3', + }); + + registerType('foo', { + isExportable: (obj) => { + if (obj.id === '1') { + throw new Error('reason'); + } + return true; + }, + }); + registerType('bar', { isExportable: () => true }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, bar3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + }); + + expect(objects).toEqual([foo2, bar3]); + expect(excludedObjects).toEqual( + [foo1].map((obj) => toExcludedObject(obj, 'predicate_error')) + ); }); it('excludes references filtered by the `isExportable` predicate', async () => { @@ -255,7 +298,7 @@ describe('collectExportedObjects', () => { }); expect(objects).toEqual([foo1, bar2]); - expect(excludedObjects).toEqual([excluded1].map(toIdTuple)); + expect(excludedObjects).toEqual([excluded1].map((obj) => toExcludedObject(obj))); }); it('excludes additional objects filtered by the `isExportable` predicate', async () => { @@ -291,7 +334,7 @@ describe('collectExportedObjects', () => { }); expect(objects).toEqual([foo1, bar2]); - expect(excludedObjects).toEqual([excluded1].map(toIdTuple)); + expect(excludedObjects).toEqual([excluded1].map((obj) => toExcludedObject(obj))); }); it('returns the missing references', async () => { @@ -729,7 +772,7 @@ describe('collectExportedObjects', () => { }); expect(objects).toEqual([foo1, bar2, dolly3]); - expect(excludedObjects).toEqual([baz4].map(toIdTuple)); + expect(excludedObjects).toEqual([baz4].map((obj) => toExcludedObject(obj))); }); }); diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index ba60ff1f43589..9cd9dd99055a0 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -35,9 +35,11 @@ interface CollectExportedObjectResult { interface ExcludedObject { id: string; type: string; - reason?: string; + reason: ExclusionReason; } +export type ExclusionReason = 'predicate_error' | 'excluded'; + export const collectExportedObjects = async ({ objects, includeReferences = true, @@ -51,7 +53,7 @@ export const collectExportedObjects = async ({ const collectedObjects: SavedObject[] = []; const collectedMissingRefs: CollectedReference[] = []; - const collectedNonExportableObjects: SavedObject[] = []; + const collectedNonExportableObjects: ExcludedObject[] = []; const alreadyProcessed: Set = new Set(); let currentObjects = objects; @@ -110,10 +112,7 @@ export const collectExportedObjects = async ({ return { objects: collectedObjects, - excludedObjects: collectedNonExportableObjects.map((obj) => ({ - type: obj.type, - id: obj.id, - })), + excludedObjects: collectedNonExportableObjects, missingRefs: collectedMissingRefs, }; }; @@ -195,14 +194,26 @@ const splitByExportability = ( isExportable: SavedObjectsExportablePredicate ) => { const exportableObjects: SavedObject[] = []; - const nonExportableObjects: SavedObject[] = []; + const nonExportableObjects: ExcludedObject[] = []; objects.forEach((obj) => { - const exportable = isExportable(obj); - if (exportable) { - exportableObjects.push(obj); - } else { - nonExportableObjects.push(obj); + try { + const exportable = isExportable(obj); + if (exportable) { + exportableObjects.push(obj); + } else { + nonExportableObjects.push({ + id: obj.id, + type: obj.type, + reason: 'excluded', + }); + } + } catch (e) { + nonExportableObjects.push({ + id: obj.id, + type: obj.type, + reason: 'predicate_error', + }); } }); From 95ffef919fa3d0701b7aa47e8e66b1259505fb6d Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Wed, 16 Jun 2021 07:54:32 +0200 Subject: [PATCH 16/17] add FTR test for errored objects --- .../export_exclusion/data.json | 19 +++++++++ .../server/plugin.ts | 3 ++ .../export_transform.ts | 40 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json index c8978c72acf49..f7015ee20251d 100644 --- a/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json +++ b/test/functional/fixtures/es_archiver/saved_objects_management/export_exclusion/data.json @@ -114,3 +114,22 @@ } } } + +{ + "type": "doc", + "value": { + "index": ".kibana", + "type": "doc", + "id": "test-is-exportable:error", + "source": { + "test-is-exportable": { + "title": "obj error", + "enabled": true + }, + "type": "test-is-exportable", + "migrationVersion": {}, + "updated_at": "2018-12-21T00:43:07.096Z", + "references": [] + } + } +} diff --git a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts index d1d7c0be016c3..15afdb229b1fd 100644 --- a/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts +++ b/test/plugin_functional/plugins/saved_object_export_transforms/server/plugin.ts @@ -169,6 +169,9 @@ export class SavedObjectExportTransformsPlugin implements Plugin { importableAndExportable: true, getTitle: (obj) => obj.attributes.title, isExportable: (obj) => { + if (obj.id === 'error') { + throw new Error('something went wrong'); + } return obj.attributes.enabled === true; }, }, diff --git a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts index bceb0a102ebd3..8381e4e5f24bf 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts @@ -251,10 +251,50 @@ export default function ({ getService }: PluginFunctionalProviderContext) { { type: 'test-is-exportable', id: '2', + reason: 'excluded', }, { type: 'test-is-exportable', id: '4', + reason: 'excluded', + }, + ]); + }); + }); + + it('exclude objects if `isExportable` throws', async () => { + await supertest + .post('/api/saved_objects/_export') + .set('kbn-xsrf', 'true') + .send({ + objects: [ + { + type: 'test-is-exportable', + id: '5', + }, + { + type: 'test-is-exportable', + id: 'error', + }, + ], + includeReferencesDeep: true, + excludeExportDetails: false, + }) + .expect(200) + .then((resp) => { + const objects = parseNdJson(resp.text); + expect(objects.length).to.eql(2); + expect([objects[0]].map((obj) => `${obj.type}:${obj.id}`)).to.eql([ + 'test-is-exportable:5', + ]); + const exportDetails = (objects[ + objects.length - 1 + ] as unknown) as SavedObjectsExportResultDetails; + expect(exportDetails.excludedObjects).to.eql([ + { + type: 'test-is-exportable', + id: 'error', + reason: 'predicate_error', }, ]); }); From 7fef4678f25024b8fb90bc2d6073426d414a0217 Mon Sep 17 00:00:00 2001 From: pgayvallet Date: Thu, 17 Jun 2021 14:23:00 +0200 Subject: [PATCH 17/17] log error if isExportable throws --- .../export/collect_exported_objects.test.ts | 71 +++++++++++++++++++ .../export/collect_exported_objects.ts | 16 ++++- .../export/saved_objects_exporter.ts | 1 + .../export_transform.ts | 2 +- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/src/core/server/saved_objects/export/collect_exported_objects.test.ts b/src/core/server/saved_objects/export/collect_exported_objects.test.ts index 0fbe018ae5d80..aab9f9134ee2c 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.test.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.test.ts @@ -9,6 +9,7 @@ import { applyExportTransformsMock } from './collect_exported_objects.test.mocks'; import { savedObjectsClientMock } from '../../mocks'; import { httpServerMock } from '../../http/http_server.mocks'; +import { loggerMock } from '../../logging/logger.mock'; import { SavedObject, SavedObjectError } from '../../../types'; import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; @@ -42,6 +43,7 @@ const toMap = (record: Record): Map => new Map(Object.e describe('collectExportedObjects', () => { let savedObjectsClient: ReturnType; let request: ReturnType; + let logger: ReturnType; let typeRegistry: SavedObjectTypeRegistry; const registerType = ( @@ -71,6 +73,7 @@ describe('collectExportedObjects', () => { typeRegistry = new SavedObjectTypeRegistry(); savedObjectsClient = savedObjectsClientMock.create(); request = httpServerMock.createKibanaRequest(); + logger = loggerMock.create(); applyExportTransformsMock.mockImplementation(({ objects }) => objects); savedObjectsClient.bulkGet.mockResolvedValue({ saved_objects: [] }); }); @@ -100,6 +103,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(applyExportTransformsMock).toHaveBeenCalledTimes(1); @@ -136,6 +140,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(fooExportable).toHaveBeenCalledTimes(2); @@ -182,6 +187,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toHaveLength(0); @@ -211,6 +217,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(objects).toEqual([foo1, bar3]); @@ -247,6 +254,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(objects).toEqual([foo2, bar3]); @@ -255,6 +263,58 @@ describe('collectExportedObjects', () => { ); }); + it('logs an error for each predicate error', async () => { + const foo1 = createObject({ + type: 'foo', + id: '1', + }); + const foo2 = createObject({ + type: 'foo', + id: '2', + }); + const foo3 = createObject({ + type: 'foo', + id: '3', + }); + + registerType('foo', { + isExportable: (obj) => { + if (obj.id !== '2') { + throw new Error('reason'); + } + return true; + }, + }); + + const { objects, excludedObjects } = await collectExportedObjects({ + objects: [foo1, foo2, foo3], + savedObjectsClient, + request, + typeRegistry, + includeReferences: true, + logger, + }); + + expect(objects).toEqual([foo2]); + expect(excludedObjects).toEqual( + [foo1, foo3].map((obj) => toExcludedObject(obj, 'predicate_error')) + ); + + expect(logger.error).toHaveBeenCalledTimes(2); + const logMessages = logger.error.mock.calls.map((call) => call[0]); + + expect( + (logMessages[0] as string).startsWith( + `Error invoking "isExportable" for object foo:1. Error was: Error: reason` + ) + ).toBe(true); + expect( + (logMessages[1] as string).startsWith( + `Error invoking "isExportable" for object foo:3. Error was: Error: reason` + ) + ).toBe(true); + }); + it('excludes references filtered by the `isExportable` predicate', async () => { const foo1 = createObject({ type: 'foo', @@ -295,6 +355,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(objects).toEqual([foo1, bar2]); @@ -331,6 +392,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(objects).toEqual([foo1, bar2]); @@ -389,6 +451,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toEqual([missing1, missing2].map(toIdTuple)); @@ -411,6 +474,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(missingRefs).toHaveLength(0); @@ -454,6 +518,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -528,6 +593,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2); @@ -592,6 +658,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -637,6 +704,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(1); @@ -700,6 +768,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(savedObjectsClient.bulkGet).toHaveBeenCalledTimes(2); @@ -769,6 +838,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: true, + logger, }); expect(objects).toEqual([foo1, bar2, dolly3]); @@ -796,6 +866,7 @@ describe('collectExportedObjects', () => { request, typeRegistry, includeReferences: false, + logger, }); expect(missingRefs).toHaveLength(0); diff --git a/src/core/server/saved_objects/export/collect_exported_objects.ts b/src/core/server/saved_objects/export/collect_exported_objects.ts index 9cd9dd99055a0..4789fd3bff67f 100644 --- a/src/core/server/saved_objects/export/collect_exported_objects.ts +++ b/src/core/server/saved_objects/export/collect_exported_objects.ts @@ -8,6 +8,7 @@ import type { SavedObject } from '../../../types'; import type { KibanaRequest } from '../../http'; +import type { Logger } from '../../logging'; import { SavedObjectsClientContract, SavedObjectsExportablePredicate } from '../types'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; import type { SavedObjectsExportTransform } from './types'; @@ -24,6 +25,8 @@ interface CollectExportedObjectOptions { request: KibanaRequest; /** export transform per type */ typeRegistry: ISavedObjectTypeRegistry; + /** logger to use to log potential errors */ + logger: Logger; } interface CollectExportedObjectResult { @@ -47,6 +50,7 @@ export const collectExportedObjects = async ({ request, typeRegistry, savedObjectsClient, + logger, }: CollectExportedObjectOptions): Promise => { const exportTransforms = buildTransforms(typeRegistry); const isExportable = buildIsExportable(typeRegistry); @@ -64,7 +68,7 @@ export const collectExportedObjects = async ({ const { exportable: untransformedExportableInitialObjects, nonExportable: nonExportableInitialObjects, - } = await splitByExportability(currentObjects, isExportable); + } = await splitByExportability(currentObjects, isExportable, logger); collectedNonExportableObjects.push(...nonExportableInitialObjects); nonExportableInitialObjects.forEach((obj) => alreadyProcessed.add(objKey(obj))); @@ -86,7 +90,7 @@ export const collectExportedObjects = async ({ const { exportable: exportableAdditionalObjects, nonExportable: nonExportableAdditionalObjects, - } = await splitByExportability(additionalObjects, isExportable); + } = await splitByExportability(additionalObjects, isExportable, logger); const allExportableObjects = [...exportableInitialObjects, ...exportableAdditionalObjects]; collectedNonExportableObjects.push(...nonExportableAdditionalObjects); collectedObjects.push(...allExportableObjects); @@ -191,7 +195,8 @@ const buildIsExportable = ( const splitByExportability = ( objects: SavedObject[], - isExportable: SavedObjectsExportablePredicate + isExportable: SavedObjectsExportablePredicate, + logger: Logger ) => { const exportableObjects: SavedObject[] = []; const nonExportableObjects: ExcludedObject[] = []; @@ -209,6 +214,11 @@ const splitByExportability = ( }); } } catch (e) { + logger.error( + `Error invoking "isExportable" for object ${obj.type}:${obj.id}. Error was: ${ + e.stack ?? e.message + }` + ); nonExportableObjects.push({ id: obj.id, type: obj.type, diff --git a/src/core/server/saved_objects/export/saved_objects_exporter.ts b/src/core/server/saved_objects/export/saved_objects_exporter.ts index c170e1842904c..211dcdc4ee62d 100644 --- a/src/core/server/saved_objects/export/saved_objects_exporter.ts +++ b/src/core/server/saved_objects/export/saved_objects_exporter.ts @@ -120,6 +120,7 @@ export class SavedObjectsExporter { request, typeRegistry: this.#typeRegistry, savedObjectsClient: this.#savedObjectsClient, + logger: this.#log, }); // sort with the provided sort function then with the default export sorting diff --git a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts index 8381e4e5f24bf..8437e050091fb 100644 --- a/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts +++ b/test/plugin_functional/test_suites/saved_objects_management/export_transform.ts @@ -262,7 +262,7 @@ export default function ({ getService }: PluginFunctionalProviderContext) { }); }); - it('exclude objects if `isExportable` throws', async () => { + it('excludes objects if `isExportable` throws', async () => { await supertest .post('/api/saved_objects/_export') .set('kbn-xsrf', 'true')