diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js index b854f5d6399f99..1810c7ce79daac 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.js @@ -5,78 +5,110 @@ */ import { uriEncode } from '../lib/uri_encode'; -import { PLUGIN_ID } from '../../../../common/constants'; -export function compatibilityShimFactory(server) { - - const logDeprecation = (msg) => { - server.log(['warning', PLUGIN_ID, 'deprecation'], msg + ' This functionality will be removed with the next major version.'); - }; - - const getSavedObjectTitle = async (objectType, savedObjectId, savedObjectsClient) => { - if (!objectType) { - throw new Error('objectType is required to determine the title from the saved object'); - } - - if (!savedObjectId) { - throw new Error('savedObjectId is required to determine the title from the saved object'); - } +/* + * TODO: Kibana 8.0: + * Remove support for parsing Saved Object details from objectType / savedObjectId + * Including support for determining the Report title from objectType / savedObjectId + * + * - `objectType` is optional, but helps differentiate the type of report in the job listing + * - `title` must be explicitly passed + * - `relativeUrls` array OR `relativeUrl` string must be passed + */ - logDeprecation('The title should be provided with the job generation request. Please use Kibana to regenerate your URLs.'); - const savedObject = await savedObjectsClient.get(objectType, savedObjectId); +const getSavedObjectTitle = async (objectType, savedObjectId, savedObjectsClient) => { + const savedObject = await savedObjectsClient.get(objectType, savedObjectId); + return savedObject.attributes.title; +}; - return savedObject.attributes.title; +const getSavedObjectRelativeUrl = (objectType, savedObjectId, queryString) => { + const appPrefixes = { + dashboard: '/dashboard/', + visualization: '/visualize/edit/', + search: '/discover/', }; + const appPrefix = appPrefixes[objectType]; + if (!appPrefix) throw new Error('Unexpected app type: ' + objectType); - const getSavedObjectRelativeUrl = (objectType, savedObjectId, queryString) => { - if (!objectType) { - throw new Error('objectType is required to determine the savedObject urlHash'); - } - - if (!savedObjectId) { - throw new Error('id is required to determine the savedObject relativeUrl'); - } - - logDeprecation('The relativeUrl should be provided with the job generation request. Please use Kibana to regenerate your URLs.'); - const appPrefixes = { - dashboard: '/dashboard/', - visualization: '/visualize/edit/', - search: '/discover/' - }; - - const appPrefix = appPrefixes[objectType]; - if (!appPrefix) throw new Error('Unexpected app type: ' + objectType); + const hash = appPrefix + uriEncode.string(savedObjectId, true); - const hash = appPrefix + uriEncode.string(savedObjectId, true); + return `/app/kibana#${hash}?${queryString || ''}`; +}; - return `/app/kibana#${hash}?${queryString || ''}`; - }; +export function compatibilityShimFactory(server, logger) { + return function compatibilityShimFactory(createJobFn) { + return async function ( + { + savedObjectId, // deprecating + queryString, // deprecating + browserTimezone, + objectType, + title, + relativeUrl, // not deprecating + relativeUrls, + layout + }, + headers, + request + ) { + if (savedObjectId && (relativeUrl || relativeUrls)) { + throw new Error(`savedObjectId should not be provided if relativeUrls are provided`); + } + if (!savedObjectId && !relativeUrl && !relativeUrls) { + throw new Error(`either relativeUrls or savedObjectId must be provided`); + } - return function compatibilityShimFactory(createJob) { - return async function ({ - objectType, - savedObjectId, - title, - relativeUrls, - queryString, - browserTimezone, - layout - }, headers, request) { + let kibanaRelativeUrls; + if (relativeUrls) { + kibanaRelativeUrls = relativeUrls; + } else if (relativeUrl) { + kibanaRelativeUrls = [ relativeUrl ]; + } else { + kibanaRelativeUrls = [getSavedObjectRelativeUrl(objectType, savedObjectId, queryString)]; + logger.warning( + `The relativeUrls have been derived from saved object parameters. ` + + `This functionality will be removed with the next major version.` + ); + } - if (objectType && savedObjectId && relativeUrls) { - throw new Error('objectType and savedObjectId should not be provided in addition to the relativeUrls'); + let reportTitle; + try { + if (title) { + reportTitle = title; + } else { + if (objectType && savedObjectId) { + reportTitle = await getSavedObjectTitle( + objectType, + savedObjectId, + request.getSavedObjectsClient() + ); + logger.warning( + `The title has been derived from saved object parameters. This ` + + `functionality will be removed with the next major version.` + ); + } else { + logger.warning( + `A title parameter should be provided with the job generation ` + + `request. Please use Kibana to regenerate your POST URL to have a ` + + `title included in the PDF.` + ); + } + } + } catch (err) { + logger.error(err); // 404 for the savedObjectId, etc + throw err; } const transformedJobParams = { objectType, - title: title || await getSavedObjectTitle(objectType, savedObjectId, request.getSavedObjectsClient()), - relativeUrls: objectType && savedObjectId ? [ getSavedObjectRelativeUrl(objectType, savedObjectId, queryString) ] : relativeUrls, + title: reportTitle, + relativeUrls: kibanaRelativeUrls, browserTimezone, - layout + layout, }; - return await createJob(transformedJobParams, headers, request); + return await createJobFn(transformedJobParams, headers, request); }; }; } diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js index d0344ee145427b..352c1b1affcdc6 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/compatibility_shim.test.js @@ -10,64 +10,89 @@ import { compatibilityShimFactory } from './compatibility_shim'; const createMockServer = () => { return { expose: jest.fn(), //fool once_per_server - log: jest.fn() + log: jest.fn(), }; }; +const createMockLogger = () => ({ + warning: jest.fn(), + error: jest.fn(), +}); + const createMockRequest = () => { return { getSavedObjectsClient: once(function () { return { - get: jest.fn() + get: jest.fn(), }; - }) + }), }; }; test(`passes title through if provided`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const title = 'test title'; const createJobMock = jest.fn(); - await compatibilityShim(createJobMock)({ title, relativeUrl: '/something' }, null, createMockRequest()); + await compatibilityShim(createJobMock)({ title, relativeUrls: ['/something'] }, null, createMockRequest()); + + expect(mockLogger.warning.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].title).toBe(title); }); - test(`gets the title from the savedObject`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); const mockRequest = createMockRequest(); const title = 'savedTitle'; mockRequest.getSavedObjectsClient().get.mockReturnValue({ attributes: { - title - } + title, + }, }); await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, mockRequest); + expect(mockLogger.warning.mock.calls.length).toBe(2); + expect(mockLogger.warning.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); + expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].title).toBe(title); }); test(`passes the objectType and savedObjectId to the savedObjectsClient`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); const mockRequest = createMockRequest(); mockRequest.getSavedObjectsClient().get.mockReturnValue({ attributes: { - title: '' - } + title: '', + }, }); const objectType = 'search'; const savedObjectId = 'abc'; - await compatibilityShim(createJobMock)({ objectType, savedObjectId, }, null, mockRequest); + await compatibilityShim(createJobMock)({ objectType, savedObjectId }, null, mockRequest); + + expect(mockLogger.warning.mock.calls.length).toBe(2); + expect(mockLogger.warning.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.warning.mock.calls[1][0]).toEqual( + 'The title has been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); const getMock = mockRequest.getSavedObjectsClient().get.mock; expect(getMock.calls.length).toBe(1); @@ -75,9 +100,50 @@ test(`passes the objectType and savedObjectId to the savedObjectsClient`, async expect(getMock.calls[0][1]).toBe(savedObjectId); }); +test(`logs no warnings when title and relativeUrl is passed`, async () => { + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); + + const createJobMock = jest.fn(); + const mockRequest = createMockRequest(); + + await compatibilityShim(createJobMock)({ title: 'Phenomenal Dashboard', relativeUrl: '/abc' }, null, mockRequest); + + expect(mockLogger.warning.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + +}); +test(`logs no warnings when title and relativeUrls is passed`, async () => { + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); + + const createJobMock = jest.fn(); + const mockRequest = createMockRequest(); + + await compatibilityShim(createJobMock)({ title: 'Phenomenal Dashboard', relativeUrls: ['/abc', '/def'] }, null, mockRequest); + + expect(mockLogger.warning.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); +}); + +test(`logs warning if title can not be provided`, async () => { + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); + + const createJobMock = jest.fn(); + const mockRequest = createMockRequest(); + await compatibilityShim(createJobMock)({ relativeUrl: '/abc' }, null, mockRequest); + + expect(mockLogger.warning.mock.calls.length).toBe(1); + expect(mockLogger.warning.mock.calls[0][0]).toEqual( + `A title parameter should be provided with the job generation request. Please ` + + `use Kibana to regenerate your POST URL to have a title included in the PDF.` + ); +}); + test(`logs deprecations when generating the title/relativeUrl using the savedObject`, async () => { - const mockServer = createMockServer(); - const compatibilityShim = compatibilityShimFactory(mockServer); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); const mockRequest = createMockRequest(); @@ -89,13 +155,18 @@ test(`logs deprecations when generating the title/relativeUrl using the savedObj await compatibilityShim(createJobMock)({ objectType: 'search', savedObjectId: 'abc' }, null, mockRequest); - expect(mockServer.log.mock.calls.length).toBe(2); - expect(mockServer.log.mock.calls[0][0]).toEqual(['warning', 'reporting', 'deprecation']); - expect(mockServer.log.mock.calls[1][0]).toEqual(['warning', 'reporting', 'deprecation']); + expect(mockLogger.warning.mock.calls.length).toBe(2); + expect(mockLogger.warning.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.warning.mock.calls[1][0]).toEqual( + 'The title has been derived from saved object parameters. This functionality will be removed with the next major version.' + ); }); test(`passes objectType through`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); const mockRequest = createMockRequest(); @@ -103,27 +174,43 @@ test(`passes objectType through`, async () => { const objectType = 'foo'; await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something', objectType }, null, mockRequest); + expect(mockLogger.warning.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].objectType).toBe(objectType); }); test(`passes the relativeUrls through`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); const relativeUrls = ['/app/kibana#something', '/app/kibana#something-else']; await compatibilityShim(createJobMock)({ title: 'test', relativeUrls }, null, null); + + expect(mockLogger.warning.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].relativeUrls).toBe(relativeUrls); }); const testSavedObjectRelativeUrl = (objectType, expectedUrl) => { test(`generates the saved object relativeUrl for ${objectType}`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); await compatibilityShim(createJobMock)({ title: 'test', objectType, savedObjectId: 'abc', }, null, null); + + expect(mockLogger.warning.mock.calls.length).toBe(1); + expect(mockLogger.warning.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); + expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual([expectedUrl]); }); @@ -134,16 +221,25 @@ testSavedObjectRelativeUrl('visualization', '/app/kibana#/visualize/edit/abc?'); testSavedObjectRelativeUrl('dashboard', '/app/kibana#/dashboard/abc?'); test(`appends the queryString to the relativeUrl when generating from the savedObject`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); await compatibilityShim(createJobMock)({ title: 'test', objectType: 'search', savedObjectId: 'abc', queryString: 'foo=bar' }, null, null); + + expect(mockLogger.warning.mock.calls.length).toBe(1); + expect(mockLogger.warning.mock.calls[0][0]).toEqual( + 'The relativeUrls have been derived from saved object parameters. This functionality will be removed with the next major version.' + ); + expect(mockLogger.error.mock.calls.length).toBe(0); + expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][0].relativeUrls).toEqual(['/app/kibana#/discover/abc?foo=bar']); }); test(`throw an Error if the objectType, savedObjectId and relativeUrls are provided`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); const promise = compatibilityShim(createJobMock)({ @@ -157,7 +253,8 @@ test(`throw an Error if the objectType, savedObjectId and relativeUrls are provi }); test(`passes headers and request through`, async () => { - const compatibilityShim = compatibilityShimFactory(createMockServer()); + const mockLogger = createMockLogger(); + const compatibilityShim = compatibilityShimFactory(createMockServer(), mockLogger); const createJobMock = jest.fn(); @@ -166,6 +263,9 @@ test(`passes headers and request through`, async () => { await compatibilityShim(createJobMock)({ title: 'test', relativeUrl: '/something' }, headers, request); + expect(mockLogger.warning.mock.calls.length).toBe(0); + expect(mockLogger.error.mock.calls.length).toBe(0); + expect(createJobMock.mock.calls.length).toBe(1); expect(createJobMock.mock.calls[0][1]).toBe(headers); expect(createJobMock.mock.calls[0][2]).toBe(request); diff --git a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js index bab11f10045522..7928d3ac345581 100644 --- a/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js +++ b/x-pack/legacy/plugins/reporting/export_types/printable_pdf/server/create_job/index.js @@ -4,26 +4,26 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PLUGIN_ID, PDF_JOB_TYPE } from '../../../../common/constants'; +import { LevelLogger, oncePerServer } from '../../../../server/lib'; import { cryptoFactory } from '../../../../server/lib/crypto'; -import { oncePerServer } from '../../../../server/lib/once_per_server'; import { compatibilityShimFactory } from './compatibility_shim'; -function createJobFn(server) { - const compatibilityShim = compatibilityShimFactory(server); +function createJobFactoryFn(server) { + const logger = LevelLogger.createForServer(server, [PLUGIN_ID, PDF_JOB_TYPE, 'create']); + const compatibilityShim = compatibilityShimFactory(server, logger); const crypto = cryptoFactory(server); - return compatibilityShim(async function createJob({ - objectType, - title, - relativeUrls, - browserTimezone, - layout - }, headers, request) { + return compatibilityShim(async function createJobFn( + { objectType, title, relativeUrls, browserTimezone, layout }, + headers, + request + ) { const serializedEncryptedHeaders = await crypto.encrypt(headers); return { - type: objectType, - title: title, + type: objectType, // Note: this changes the shape of the job params object + title, objects: relativeUrls.map(u => ({ relativeUrl: u })), headers: serializedEncryptedHeaders, browserTimezone, @@ -34,4 +34,4 @@ function createJobFn(server) { }); } -export const createJobFactory = oncePerServer(createJobFn); +export const createJobFactory = oncePerServer(createJobFactoryFn);