From 24db901afacc922fd185b8dedc5fd154dc46295b Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 13 Dec 2021 11:11:28 -0500 Subject: [PATCH] [7.15] [Cases] Fix remark stringify version to match remark parse (#119995) (#120916) * [Cases] Fix remark stringify version to match remark parse (#119995) * match parse and stringify version. try/catch added * Adding tests and refactoring logError * Adding relative path to core and kibana utils * remark curstom serializers adapted to version 8 * add error logging to comments migration * Adding tests for mergeMigrationFunctionMap logging Co-authored-by: Jonathan Buttner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # package.json # x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts # x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts # x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts # x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts # x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts # x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts # x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts # yarn.lock * Adding tests and fixing yarn lock * Using correct migration * fixing eslint issues * Removing redudent tests Co-authored-by: Sergi Massaneda Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- package.json | 2 +- .../utils/markdown_plugins/lens/serializer.ts | 8 +- .../markdown_plugins/timeline/serializer.ts | 8 +- .../utils/markdown_plugins/utils.test.ts | 110 +++++ .../common/utils/markdown_plugins/utils.ts | 42 +- .../migrations/comments.test.ts | 399 ++++++++++++++++++ .../saved_object_types/migrations/comments.ts | 178 ++++++++ .../migrations/index.test.ts | 245 ----------- .../saved_object_types/migrations/index.ts | 117 +---- .../migrations/utils.test.ts | 36 +- .../saved_object_types/migrations/utils.ts | 39 +- yarn.lock | 43 +- 12 files changed, 843 insertions(+), 384 deletions(-) create mode 100644 x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts create mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts delete mode 100644 x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts diff --git a/package.json b/package.json index 4bb61901d75ef..f32ba02c7386a 100644 --- a/package.json +++ b/package.json @@ -380,7 +380,7 @@ "redux-thunks": "^1.0.0", "regenerator-runtime": "^0.13.3", "remark-parse": "^8.0.3", - "remark-stringify": "^9.0.0", + "remark-stringify": "^8.0.3", "request": "^2.88.0", "require-in-the-middle": "^5.0.2", "reselect": "^4.0.0", diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts index e561b2f8cfb8a..69d01b0051e18 100644 --- a/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/lens/serializer.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { Plugin } from 'unified'; import type { TimeRange } from 'src/plugins/data/common'; import { LENS_ID } from './constants'; @@ -13,8 +14,13 @@ export interface LensSerializerProps { timeRange: TimeRange; } -export const LensSerializer = ({ timeRange, attributes }: LensSerializerProps) => +const serializeLens = ({ timeRange, attributes }: LensSerializerProps) => `!{${LENS_ID}${JSON.stringify({ timeRange, attributes, })}}`; + +export const LensSerializer: Plugin = function () { + const Compiler = this.Compiler; + Compiler.prototype.visitors.lens = serializeLens; +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts index 0a95c9466b1ff..b9448f93d95c3 100644 --- a/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/timeline/serializer.ts @@ -5,8 +5,14 @@ * 2.0. */ +import { Plugin } from 'unified'; export interface TimelineSerializerProps { match: string; } -export const TimelineSerializer = ({ match }: TimelineSerializerProps) => match; +const serializeTimeline = ({ match }: TimelineSerializerProps) => match; + +export const TimelineSerializer: Plugin = function () { + const Compiler = this.Compiler; + Compiler.prototype.visitors.timeline = serializeTimeline; +}; diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts new file mode 100644 index 0000000000000..8c9e2eccc3b65 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.test.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { parseCommentString, stringifyComment } from './utils'; + +describe('markdown utils', () => { + describe('stringifyComment', () => { + it('adds a newline to the end if one does not exist', () => { + const parsed = parseCommentString('hello'); + expect(stringifyComment(parsed)).toEqual('hello\n'); + }); + + it('does not add a newline to the end if one already exists', () => { + const parsed = parseCommentString('hello\n'); + expect(stringifyComment(parsed)).toEqual('hello\n'); + }); + + // This check ensures the version of remark-stringify supports tables. From version 9+ this is not supported by default. + it('parses and stringifies github formatted markdown correctly', () => { + const parsed = parseCommentString(`| Tables | Are | Cool | + |----------|:-------------:|------:| + | col 1 is | left-aligned | $1600 | + | col 2 is | centered | $12 | + | col 3 is | right-aligned | $1 |`); + + expect(stringifyComment(parsed)).toMatchInlineSnapshot(` + "| Tables | Are | Cool | + | -------- | :-----------: | ----: | + | col 1 is | left-aligned | $1600 | + | col 2 is | centered | $12 | + | col 3 is | right-aligned | $1 | + " + `); + }); + + it('parses a timeline url', () => { + const timelineUrl = + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))'; + + const parsedNodes = parseCommentString(timelineUrl); + + expect(parsedNodes).toMatchInlineSnapshot(` + Object { + "children": Array [ + Object { + "match": "[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))", + "position": Position { + "end": Object { + "column": 138, + "line": 1, + "offset": 137, + }, + "indent": Array [], + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "timeline", + }, + ], + "position": Object { + "end": Object { + "column": 138, + "line": 1, + "offset": 137, + }, + "start": Object { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", + } + `); + }); + + it('stringifies a timeline url', () => { + const timelineUrl = + '[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))'; + + const parsedNodes = parseCommentString(timelineUrl); + + expect(stringifyComment(parsedNodes)).toEqual(`${timelineUrl}\n`); + }); + + it('parses a lens visualization', () => { + const lensVisualization = + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"TEst22","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}}}}}},"visualization":{"layerId":"layer1","accessor":"col2"},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-layer1"}]}}}'; + + const parsedNodes = parseCommentString(lensVisualization); + expect(parsedNodes.children[0].type).toEqual('lens'); + }); + + it('stringifies a lens visualization', () => { + const lensVisualization = + '!{lens{"timeRange":{"from":"now-7d","to":"now","mode":"relative"},"attributes":{"title":"TEst22","type":"lens","visualizationType":"lnsMetric","state":{"datasourceStates":{"indexpattern":{"layers":{"layer1":{"columnOrder":["col2"],"columns":{"col2":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}}}}}},"visualization":{"layerId":"layer1","accessor":"col2"},"query":{"language":"kuery","query":""},"filters":[]},"references":[{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-current-indexpattern"},{"type":"index-pattern","id":"90943e30-9a47-11e8-b64d-95841ca0b247","name":"indexpattern-datasource-layer-layer1"}]}}}'; + + const parsedNodes = parseCommentString(lensVisualization); + + expect(stringifyComment(parsedNodes)).toEqual(`${lensVisualization}\n`); + }); + }); +}); diff --git a/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts index e9a44fd592846..f4f689bba52e7 100644 --- a/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts +++ b/x-pack/plugins/cases/common/utils/markdown_plugins/utils.ts @@ -16,14 +16,17 @@ import { SerializableRecord } from '@kbn/utility-types'; import { LENS_ID, LensParser, LensSerializer } from './lens'; import { TimelineSerializer, TimelineParser } from './timeline'; -interface LensMarkdownNode extends Node { +export interface LensMarkdownNode extends Node { timeRange: TimeRange; attributes: SerializableRecord; type: string; id: string; } -interface LensMarkdownParent extends Node { +/** + * A node that has children of other nodes describing the markdown elements or a specific lens visualization. + */ +export interface MarkdownNode extends Node { children: Array; } @@ -32,25 +35,28 @@ export const getLensVisualizations = (parsedComment?: Array { const processor = unified().use([[markdown, {}], LensParser, TimelineParser]); - return processor.parse(comment) as LensMarkdownParent; + return processor.parse(comment) as MarkdownNode; }; -export const stringifyComment = (comment: LensMarkdownParent) => +export const stringifyComment = (comment: MarkdownNode) => unified() .use([ - [ - remarkStringify, - { - allowDangerousHtml: true, - handlers: { - /* - because we're using rison in the timeline url we need - to make sure that markdown parser doesn't modify the url - */ - timeline: TimelineSerializer, - lens: LensSerializer, - }, - }, - ], + [remarkStringify], + /* + because we're using rison in the timeline url we need + to make sure that markdown parser doesn't modify the url + */ + LensSerializer, + TimelineSerializer, ]) .stringify(comment); + +export const isLensMarkdownNode = (node?: unknown): node is LensMarkdownNode => { + const unsafeNode = node as LensMarkdownNode; + return ( + unsafeNode != null && + unsafeNode.timeRange != null && + unsafeNode.attributes != null && + unsafeNode.type === 'lens' + ); +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts new file mode 100644 index 0000000000000..8f103a20f2070 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.test.ts @@ -0,0 +1,399 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + createCommentsMigrations, + mergeMigrationFunctionMaps, + migrateByValueLensVisualizations, + stringifyCommentWithoutTrailingNewline, +} from './comments'; +import { + getLensVisualizations, + parseCommentString, +} from '../../../common/utils/markdown_plugins/utils'; + +import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks'; +import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory'; +import { LensDocShape715 } from '../../../../lens/server'; +import { + SavedObjectReference, + SavedObjectsMigrationLogger, + SavedObjectUnsanitizedDoc, +} from 'kibana/server'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; +import { SerializableRecord } from '@kbn/utility-types'; + +describe('comments migrations', () => { + const migrations = createCommentsMigrations({ + lensEmbeddableFactory, + }); + + const contextMock = savedObjectsServiceMock.createMigrationContext(); + + const lensVisualizationToMigrate = { + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto', timeZone: 'Europe/Berlin' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('lens embeddable migrations for by value panels', () => { + describe('7.14.0 remove time zone from Lens visualization date histogram', () => { + const expectedLensVisualizationMigrated = { + title: 'MyRenamedOps', + description: '', + visualizationType: 'lnsXY', + state: { + datasourceStates: { + indexpattern: { + layers: { + '2': { + columns: { + '3': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '4': { + label: '@timestamp', + dataType: 'date', + operationType: 'date_histogram', + sourceField: '@timestamp', + isBucketed: true, + scale: 'interval', + params: { interval: 'auto' }, + }, + '5': { + label: '@timestamp', + dataType: 'date', + operationType: 'my_unexpected_operation', + isBucketed: true, + scale: 'interval', + params: { timeZone: 'do not delete' }, + }, + }, + columnOrder: ['3', '4', '5'], + incompleteColumns: {}, + }, + }, + }, + }, + visualization: { + title: 'Empty XY chart', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', + preferredSeriesType: 'bar_stacked', + layers: [ + { + layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', + accessors: [ + '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', + 'e5efca70-edb5-4d6d-a30a-79384066987e', + '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', + ], + position: 'top', + seriesType: 'bar_stacked', + showGridlines: false, + xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', + }, + ], + }, + query: { query: '', language: 'kuery' }, + filters: [], + }, + }; + + const expectedMigrationCommentResult = `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":${JSON.stringify( + expectedLensVisualizationMigrated + )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr"`; + + const caseComment = { + type: 'cases-comments', + id: '1cefd0d0-e86d-11eb-bae5-3d065cd16a32', + attributes: { + associationType: 'case', + comment: `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":${JSON.stringify( + lensVisualizationToMigrate + )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr"`, + type: 'user', + created_at: '2021-07-19T08:41:29.951Z', + created_by: { + email: null, + full_name: null, + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2021-07-19T08:41:47.549Z', + updated_by: { + full_name: null, + email: null, + username: 'elastic', + }, + }, + references: [ + { + name: 'associated-cases', + id: '77d1b230-d35e-11eb-8da6-6f746b9cb499', + type: 'cases', + }, + { + name: 'indexpattern-datasource-current-indexpattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'index-pattern', + }, + { + name: 'indexpattern-datasource-current-indexpattern', + id: '90943e30-9a47-11e8-b64d-95841ca0b247', + type: 'index-pattern', + }, + ], + migrationVersion: { + 'cases-comments': '7.14.0', + }, + coreMigrationVersion: '8.0.0', + updated_at: '2021-07-19T08:41:47.552Z', + version: 'WzgxMTY4MSw5XQ==', + namespaces: ['default'], + score: 0, + }; + + it('should remove time zone param from date histogram', () => { + expect(migrations['7.14.0']).toBeDefined(); + const result = migrations['7.14.0'](caseComment, contextMock); + + const parsedComment = parseCommentString(result.attributes.comment); + const lensVisualizations = (getLensVisualizations( + parsedComment.children + ) as unknown) as Array<{ + attributes: LensDocShape715 & { references: SavedObjectReference[] }; + }>; + + const layers = Object.values( + lensVisualizations[0].attributes.state.datasourceStates.indexpattern.layers + ); + expect(result.attributes.comment).toEqual(expectedMigrationCommentResult); + expect(layers.length).toBe(1); + const columns = Object.values(layers[0].columns); + expect(columns.length).toBe(3); + expect(columns[0].operationType).toEqual('date_histogram'); + expect((columns[0] as { params: {} }).params).toEqual({ interval: 'auto' }); + expect(columns[1].operationType).toEqual('date_histogram'); + expect((columns[1] as { params: {} }).params).toEqual({ interval: 'auto' }); + expect(columns[2].operationType).toEqual('my_unexpected_operation'); + expect((columns[2] as { params: {} }).params).toEqual({ timeZone: 'do not delete' }); + }); + }); + }); + + describe('handles errors', () => { + interface CommentSerializable extends SerializableRecord { + comment?: string; + } + + const migrationFunction: MigrateFunction = ( + comment + ) => { + throw new Error('an error'); + }; + + const comment = `!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":${JSON.stringify( + lensVisualizationToMigrate + )}}}\n\n`; + + const caseComment = { + type: 'cases-comments', + id: '1cefd0d0-e86d-11eb-bae5-3d065cd16a32', + attributes: { + comment, + }, + references: [], + }; + + it('logs an error when it fails to parse invalid json', () => { + const commentMigrationFunction = migrateByValueLensVisualizations(migrationFunction, '1.0.0'); + + const result = commentMigrationFunction(caseComment, contextMock); + // the comment should remain unchanged when there is an error + expect(result.attributes.comment).toEqual(comment); + + const log = contextMock.log as jest.Mocked; + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate comment with doc id: 1cefd0d0-e86d-11eb-bae5-3d065cd16a32 version: 8.0.0 error: an error", + Object { + "migrations": Object { + "comment": Object { + "id": "1cefd0d0-e86d-11eb-bae5-3d065cd16a32", + }, + }, + }, + ] + `); + }); + + describe('mergeMigrationFunctionMaps', () => { + it('logs an error when the passed migration functions fails', () => { + const migrationObj1 = ({ + '1.0.0': migrateByValueLensVisualizations(migrationFunction, '1.0.0'), + } as unknown) as MigrateFunctionsObject; + + const migrationObj2 = { + '2.0.0': (doc: SavedObjectUnsanitizedDoc<{ comment?: string }>) => { + return doc; + }, + }; + + const mergedFunctions = mergeMigrationFunctionMaps(migrationObj1, migrationObj2); + mergedFunctions['1.0.0'](caseComment, contextMock); + + const log = contextMock.log as jest.Mocked; + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate comment with doc id: 1cefd0d0-e86d-11eb-bae5-3d065cd16a32 version: 8.0.0 error: an error", + Object { + "migrations": Object { + "comment": Object { + "id": "1cefd0d0-e86d-11eb-bae5-3d065cd16a32", + }, + }, + }, + ] + `); + }); + + it('it does not log an error when the migration function does not use the context', () => { + const migrationObj1 = ({ + '1.0.0': migrateByValueLensVisualizations(migrationFunction, '1.0.0'), + } as unknown) as MigrateFunctionsObject; + + const migrationObj2 = { + '2.0.0': (doc: SavedObjectUnsanitizedDoc<{ comment?: string }>) => { + throw new Error('2.0.0 error'); + }, + }; + + const mergedFunctions = mergeMigrationFunctionMaps(migrationObj1, migrationObj2); + + expect(() => mergedFunctions['2.0.0'](caseComment, contextMock)).toThrow(); + + const log = contextMock.log as jest.Mocked; + expect(log.error).not.toHaveBeenCalled(); + }); + }); + }); + + describe('stringifyCommentWithoutTrailingNewline', () => { + it('removes the newline added by the markdown library when the comment did not originally have one', () => { + const originalComment = 'awesome'; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome' + ); + }); + + it('leaves the newline if it was in the original comment', () => { + const originalComment = 'awesome\n'; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome\n' + ); + }); + + it('does not remove newlines that are not at the end of the comment', () => { + const originalComment = 'awesome\ncomment'; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome\ncomment' + ); + }); + + it('does not remove spaces at the end of the comment', () => { + const originalComment = 'awesome '; + const parsedString = parseCommentString(originalComment); + + expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual( + 'awesome ' + ); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts new file mode 100644 index 0000000000000..4c8daa0525862 --- /dev/null +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts @@ -0,0 +1,178 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mapValues, trimEnd, mergeWith } from 'lodash'; +import type { SerializableRecord } from '@kbn/utility-types'; +import { + MigrateFunction, + MigrateFunctionsObject, +} from '../../../../../../src/plugins/kibana_utils/common'; +import { + SavedObjectUnsanitizedDoc, + SavedObjectSanitizedDoc, + SavedObjectMigrationFn, + SavedObjectMigrationMap, + SavedObjectMigrationContext, +} from '../../../../../../src/core/server'; +import { LensServerPluginSetup } from '../../../../lens/server'; +import { CommentType, AssociationType } from '../../../common/api'; +import { + isLensMarkdownNode, + LensMarkdownNode, + MarkdownNode, + parseCommentString, + stringifyComment, +} from '../../../common/utils/markdown_plugins/utils'; +import { addOwnerToSO, SanitizedCaseOwner } from '.'; +import { logError } from './utils'; + +interface UnsanitizedComment { + comment: string; + type?: CommentType; +} + +interface SanitizedComment { + comment: string; + type: CommentType; +} + +interface SanitizedCommentForSubCases { + associationType: AssociationType; + rule?: { id: string | null; name: string | null }; +} + +export interface CreateCommentsMigrationsDeps { + lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; +} + +export const createCommentsMigrations = ( + migrationDeps: CreateCommentsMigrationsDeps +): SavedObjectMigrationMap => { + const embeddableMigrations = mapValues< + MigrateFunctionsObject, + SavedObjectMigrationFn<{ comment?: string }> + >( + migrationDeps.lensEmbeddableFactory().migrations, + migrateByValueLensVisualizations + ) as MigrateFunctionsObject; + + const commentsMigrations = { + '7.11.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + return { + ...doc, + attributes: { + ...doc.attributes, + type: CommentType.user, + }, + references: doc.references || [], + }; + }, + '7.12.0': ( + doc: SavedObjectUnsanitizedDoc + ): SavedObjectSanitizedDoc => { + let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { + ...doc.attributes, + associationType: AssociationType.case, + }; + + // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are + // introduced in 7.12. + if (doc.attributes.type === CommentType.alert) { + attributes = { ...attributes, rule: { id: null, name: null } }; + } + + return { + ...doc, + attributes, + references: doc.references || [], + }; + }, + '7.14.0': ( + doc: SavedObjectUnsanitizedDoc> + ): SavedObjectSanitizedDoc => { + return addOwnerToSO(doc); + }, + }; + + return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations); +}; + +export const migrateByValueLensVisualizations = ( + migrate: MigrateFunction, + version: string +): SavedObjectMigrationFn<{ comment?: string }, { comment?: string }> => ( + doc: SavedObjectUnsanitizedDoc<{ comment?: string }>, + context: SavedObjectMigrationContext +) => { + if (doc.attributes.comment == null) { + return doc; + } + + try { + const parsedComment = parseCommentString(doc.attributes.comment); + const migratedComment = parsedComment.children.map((comment) => { + if (isLensMarkdownNode(comment)) { + // casting here because ts complains that comment isn't serializable because LensMarkdownNode + // extends Node which has fields that conflict with SerializableRecord even though it is serializable + return migrate(comment as SerializableRecord) as LensMarkdownNode; + } + + return comment; + }); + + const migratedMarkdown = { ...parsedComment, children: migratedComment }; + + return { + ...doc, + attributes: { + ...doc.attributes, + comment: stringifyCommentWithoutTrailingNewline(doc.attributes.comment, migratedMarkdown), + }, + }; + } catch (error) { + logError({ id: doc.id, context, error, docType: 'comment', docKey: 'comment' }); + return doc; + } +}; + +export const stringifyCommentWithoutTrailingNewline = ( + originalComment: string, + markdownNode: MarkdownNode +) => { + const stringifiedComment = stringifyComment(markdownNode); + + // if the original comment already ended with a newline then just leave it there + if (originalComment.endsWith('\n')) { + return stringifiedComment; + } + + // the original comment did not end with a newline so the markdown library is going to add one, so let's remove it + // so the comment stays consistent + return trimEnd(stringifiedComment, '\n'); +}; + +/** + * merge function maps adds the context param from the original implementation at: + * src/plugins/kibana_utils/common/persistable_state/merge_migration_function_map.ts + * */ +export const mergeMigrationFunctionMaps = ( + // using the saved object framework types here because they include the context, this avoids type errors in our tests + obj1: SavedObjectMigrationMap, + obj2: SavedObjectMigrationMap +) => { + const customizer = (objValue: SavedObjectMigrationFn, srcValue: SavedObjectMigrationFn) => { + if (!srcValue || !objValue) { + return srcValue || objValue; + } + return (doc: SavedObjectUnsanitizedDoc, context: SavedObjectMigrationContext) => + objValue(srcValue(doc, context), context); + }; + + return mergeWith({ ...obj1 }, obj2, customizer); +}; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts deleted file mode 100644 index 151c340297ceb..0000000000000 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.test.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createCommentsMigrations } from './index'; -import { - getLensVisualizations, - parseCommentString, -} from '../../../common/utils/markdown_plugins/utils'; - -import { savedObjectsServiceMock } from '../../../../../../src/core/server/mocks'; -import { lensEmbeddableFactory } from '../../../../lens/server/embeddable/lens_embeddable_factory'; -import { LensDocShape715 } from '../../../../lens/server'; -import { SavedObjectReference } from 'kibana/server'; - -const migrations = createCommentsMigrations({ - lensEmbeddableFactory, -}); - -const contextMock = savedObjectsServiceMock.createMigrationContext(); - -describe('lens embeddable migrations for by value panels', () => { - describe('7.14.0 remove time zone from Lens visualization date histogram', () => { - const lensVisualizationToMigrate = { - title: 'MyRenamedOps', - description: '', - visualizationType: 'lnsXY', - state: { - datasourceStates: { - indexpattern: { - layers: { - '2': { - columns: { - '3': { - label: '@timestamp', - dataType: 'date', - operationType: 'date_histogram', - sourceField: '@timestamp', - isBucketed: true, - scale: 'interval', - params: { interval: 'auto', timeZone: 'Europe/Berlin' }, - }, - '4': { - label: '@timestamp', - dataType: 'date', - operationType: 'date_histogram', - sourceField: '@timestamp', - isBucketed: true, - scale: 'interval', - params: { interval: 'auto' }, - }, - '5': { - label: '@timestamp', - dataType: 'date', - operationType: 'my_unexpected_operation', - isBucketed: true, - scale: 'interval', - params: { timeZone: 'do not delete' }, - }, - }, - columnOrder: ['3', '4', '5'], - incompleteColumns: {}, - }, - }, - }, - }, - visualization: { - title: 'Empty XY chart', - legend: { isVisible: true, position: 'right' }, - valueLabels: 'hide', - preferredSeriesType: 'bar_stacked', - layers: [ - { - layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', - accessors: [ - '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', - 'e5efca70-edb5-4d6d-a30a-79384066987e', - '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', - ], - position: 'top', - seriesType: 'bar_stacked', - showGridlines: false, - xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', - }, - ], - }, - query: { query: '', language: 'kuery' }, - filters: [], - }, - }; - - const expectedLensVisualizationMigrated = { - title: 'MyRenamedOps', - description: '', - visualizationType: 'lnsXY', - state: { - datasourceStates: { - indexpattern: { - layers: { - '2': { - columns: { - '3': { - label: '@timestamp', - dataType: 'date', - operationType: 'date_histogram', - sourceField: '@timestamp', - isBucketed: true, - scale: 'interval', - params: { interval: 'auto' }, - }, - '4': { - label: '@timestamp', - dataType: 'date', - operationType: 'date_histogram', - sourceField: '@timestamp', - isBucketed: true, - scale: 'interval', - params: { interval: 'auto' }, - }, - '5': { - label: '@timestamp', - dataType: 'date', - operationType: 'my_unexpected_operation', - isBucketed: true, - scale: 'interval', - params: { timeZone: 'do not delete' }, - }, - }, - columnOrder: ['3', '4', '5'], - incompleteColumns: {}, - }, - }, - }, - }, - visualization: { - title: 'Empty XY chart', - legend: { isVisible: true, position: 'right' }, - valueLabels: 'hide', - preferredSeriesType: 'bar_stacked', - layers: [ - { - layerId: '5ab74ddc-93ca-44e2-9857-ecf85c86b53e', - accessors: [ - '5fea2a56-7b73-44b5-9a50-7f0c0c4f8fd0', - 'e5efca70-edb5-4d6d-a30a-79384066987e', - '7ffb7bde-4f42-47ab-b74d-1b4fd8393e0f', - ], - position: 'top', - seriesType: 'bar_stacked', - showGridlines: false, - xAccessor: '2e57a41e-5a52-42d3-877f-bd211d903ef8', - }, - ], - }, - query: { query: '', language: 'kuery' }, - filters: [], - }, - }; - - const expectedMigrationCommentResult = `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":${JSON.stringify( - expectedLensVisualizationMigrated - )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr" -`; - - const caseComment = { - type: 'cases-comments', - id: '1cefd0d0-e86d-11eb-bae5-3d065cd16a32', - attributes: { - associationType: 'case', - comment: `"**Amazing**\n\n!{tooltip[Tessss](https://example.com)}\n\nbrbrbr\n\n[asdasdasdasd](http://localhost:5601/moq/app/security/timelines?timeline=(id%3A%27e4362a60-f478-11eb-a4b0-ebefce184d8d%27%2CisOpen%3A!t))\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":${JSON.stringify( - lensVisualizationToMigrate - )}}}\n\n!{lens{\"timeRange\":{\"from\":\"now-7d\",\"to\":\"now\",\"mode\":\"relative\"},\"editMode\":false,\"attributes\":{\"title\":\"TEst22\",\"type\":\"lens\",\"visualizationType\":\"lnsMetric\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"layer1\":{\"columnOrder\":[\"col2\"],\"columns\":{\"col2\":{\"dataType\":\"number\",\"isBucketed\":false,\"label\":\"Count of records\",\"operationType\":\"count\",\"scale\":\"ratio\",\"sourceField\":\"Records\"}}}}}},\"visualization\":{\"layerId\":\"layer1\",\"accessor\":\"col2\"},\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"90943e30-9a47-11e8-b64d-95841ca0b247\",\"name\":\"indexpattern-datasource-layer-layer1\"}]}}}\n\nbrbrbr"`, - type: 'user', - created_at: '2021-07-19T08:41:29.951Z', - created_by: { - email: null, - full_name: null, - username: 'elastic', - }, - pushed_at: null, - pushed_by: null, - updated_at: '2021-07-19T08:41:47.549Z', - updated_by: { - full_name: null, - email: null, - username: 'elastic', - }, - }, - references: [ - { - name: 'associated-cases', - id: '77d1b230-d35e-11eb-8da6-6f746b9cb499', - type: 'cases', - }, - { - name: 'indexpattern-datasource-current-indexpattern', - id: '90943e30-9a47-11e8-b64d-95841ca0b247', - type: 'index-pattern', - }, - { - name: 'indexpattern-datasource-current-indexpattern', - id: '90943e30-9a47-11e8-b64d-95841ca0b247', - type: 'index-pattern', - }, - ], - migrationVersion: { - 'cases-comments': '7.14.0', - }, - coreMigrationVersion: '8.0.0', - updated_at: '2021-07-19T08:41:47.552Z', - version: 'WzgxMTY4MSw5XQ==', - namespaces: ['default'], - score: 0, - }; - - it('should remove time zone param from date histogram', () => { - expect(migrations['7.14.0']).toBeDefined(); - const result = migrations['7.14.0'](caseComment, contextMock); - - const parsedComment = parseCommentString(result.attributes.comment); - const lensVisualizations = (getLensVisualizations( - parsedComment.children - ) as unknown) as Array<{ - attributes: LensDocShape715 & { references: SavedObjectReference[] }; - }>; - - const layers = Object.values( - lensVisualizations[0].attributes.state.datasourceStates.indexpattern.layers - ); - expect(result.attributes.comment).toEqual(expectedMigrationCommentResult); - expect(layers.length).toBe(1); - const columns = Object.values(layers[0].columns); - expect(columns.length).toBe(3); - expect(columns[0].operationType).toEqual('date_histogram'); - expect((columns[0] as { params: {} }).params).toEqual({ interval: 'auto' }); - expect(columns[1].operationType).toEqual('date_histogram'); - expect((columns[1] as { params: {} }).params).toEqual({ interval: 'auto' }); - expect(columns[2].operationType).toEqual('my_unexpected_operation'); - expect((columns[2] as { params: {} }).params).toEqual({ timeZone: 'do not delete' }); - }); - }); -}); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts index 751f9e11f7370..475782dd55c47 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -7,30 +7,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { flow, mapValues } from 'lodash'; -import { LensServerPluginSetup } from '../../../../lens/server'; - -import { - mergeMigrationFunctionMaps, - MigrateFunction, - MigrateFunctionsObject, -} from '../../../../../../src/plugins/kibana_utils/common'; import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, - SavedObjectMigrationFn, - SavedObjectMigrationMap, } from '../../../../../../src/core/server'; -import { - ConnectorTypes, - CommentType, - AssociationType, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; -import { parseCommentString, stringifyComment } from '../../../common/utils/markdown_plugins/utils'; +import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; export { caseMigrations } from './cases'; export { configureMigrations } from './configuration'; +export type { CreateCommentsMigrationsDeps } from './comments'; +export { createCommentsMigrations } from './comments'; interface UserActions { action_field: string[]; @@ -99,103 +85,6 @@ export const userActionsMigrations = { }, }; -interface UnsanitizedComment { - comment: string; - type?: CommentType; -} - -interface SanitizedComment { - comment: string; - type: CommentType; -} - -interface SanitizedCommentForSubCases { - associationType: AssociationType; - rule?: { id: string | null; name: string | null }; -} - -const migrateByValueLensVisualizations = ( - migrate: MigrateFunction, - version: string -): SavedObjectMigrationFn => (doc: any) => { - const parsedComment = parseCommentString(doc.attributes.comment); - const migratedComment = parsedComment.children.map((comment) => { - if (comment?.type === 'lens') { - // @ts-expect-error - return migrate(comment); - } - - return comment; - }); - - // @ts-expect-error - parsedComment.children = migratedComment; - doc.attributes.comment = stringifyComment(parsedComment); - - return doc; -}; - -export interface CreateCommentsMigrationsDeps { - lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; -} - -export const createCommentsMigrations = ( - migrationDeps: CreateCommentsMigrationsDeps -): SavedObjectMigrationMap => { - const embeddableMigrations = mapValues( - migrationDeps.lensEmbeddableFactory().migrations, - migrateByValueLensVisualizations - ) as MigrateFunctionsObject; - - const commentsMigrations = { - '7.11.0': flow( - ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - return { - ...doc, - attributes: { - ...doc.attributes, - type: CommentType.user, - }, - references: doc.references || [], - }; - } - ), - '7.12.0': flow( - ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - let attributes: SanitizedCommentForSubCases & UnsanitizedComment = { - ...doc.attributes, - associationType: AssociationType.case, - }; - - // only add the rule object for alert comments. Prior to 7.12 we only had CommentType.alert, generated alerts are - // introduced in 7.12. - if (doc.attributes.type === CommentType.alert) { - attributes = { ...attributes, rule: { id: null, name: null } }; - } - - return { - ...doc, - attributes, - references: doc.references || [], - }; - } - ), - '7.14.0': flow( - ( - doc: SavedObjectUnsanitizedDoc> - ): SavedObjectSanitizedDoc => { - return addOwnerToSO(doc); - } - ), - }; - - return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations); -}; - export const connectorMappingsMigrations = { '7.14.0': ( doc: SavedObjectUnsanitizedDoc> diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts index f591bef6b3236..8ab65a1d4d95f 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.test.ts @@ -5,9 +5,15 @@ * 2.0. */ +import { SavedObjectsMigrationLogger } from 'kibana/server'; +import { migrationMocks } from '../../../../../../src/core/server/mocks'; import { noneConnectorId } from '../../../common'; import { createExternalService, createJiraConnector } from '../../services/test_utils'; -import { transformConnectorIdToReference, transformPushConnectorIdToReference } from './utils'; +import { + logError, + transformConnectorIdToReference, + transformPushConnectorIdToReference, +} from './utils'; describe('migration utils', () => { describe('transformConnectorIdToReference', () => { @@ -226,4 +232,32 @@ describe('migration utils', () => { `); }); }); + + describe('logError', () => { + const context = migrationMocks.createContext(); + it('logs an error', () => { + const log = context.log as jest.Mocked; + + logError({ + id: '1', + context, + error: new Error('an error'), + docType: 'a document', + docKey: 'key', + }); + + expect(log.error.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "Failed to migrate a document with doc id: 1 version: 8.0.0 error: an error", + Object { + "migrations": Object { + "key": Object { + "id": "1", + }, + }, + }, + ] + `); + }); + }); }); diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts index 0100a04cde679..ce2e6155b1b87 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/utils.ts @@ -8,7 +8,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { noneConnectorId } from '../../../common'; -import { SavedObjectReference } from '../../../../../../src/core/server'; +import { + SavedObjectReference, + SavedObjectMigrationContext, + LogMeta, +} from '../../../../../../src/core/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { getNoneCaseConnector, @@ -71,3 +75,36 @@ export const transformPushConnectorIdToReference = ( references, }; }; + +interface MigrationLogMeta extends LogMeta { + migrations: { + [x: string]: { + id: string; + }; + }; +} + +export function logError({ + id, + context, + error, + docType, + docKey, +}: { + id: string; + context: SavedObjectMigrationContext; + error: Error; + docType: string; + docKey: string; +}) { + context.log.error( + `Failed to migrate ${docType} with doc id: ${id} version: ${context.migrationVersion} error: ${error.message}`, + { + migrations: { + [docKey]: { + id, + }, + }, + } + ); +} diff --git a/yarn.lock b/yarn.lock index 20c2bb6042c82..4f03d5a03df46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16396,6 +16396,11 @@ is-alphabetical@1.0.4, is-alphabetical@^1.0.0: resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d" integrity sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg== +is-alphanumeric@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-alphanumeric/-/is-alphanumeric-1.0.0.tgz#4a9cef71daf4c001c1d81d63d140cf53fd6889f4" + integrity sha1-Spzvcdr0wAHB2B1j0UDPU/1oifQ= + is-alphanumerical@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-1.0.1.tgz#dfb4aa4d1085e33bdb61c2dee9c80e9c6c19f53b" @@ -18937,7 +18942,7 @@ long@^4.0.0: resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== -longest-streak@^2.0.0: +longest-streak@^2.0.0, longest-streak@^2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== @@ -19167,6 +19172,13 @@ markdown-it@^11.0.0: mdurl "^1.0.1" uc.micro "^1.0.5" +markdown-table@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-2.0.0.tgz#194a90ced26d31fe753d8b9434430214c011865b" + integrity sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A== + dependencies: + repeat-string "^1.0.0" + markdown-to-jsx@^6.11.4: version "6.11.4" resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-6.11.4.tgz#b4528b1ab668aef7fe61c1535c27e837819392c5" @@ -19234,6 +19246,13 @@ mdast-squeeze-paragraphs@^4.0.0: dependencies: unist-util-remove "^2.0.0" +mdast-util-compact@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-compact/-/mdast-util-compact-2.0.1.tgz#cabc69a2f43103628326f35b1acf735d55c99490" + integrity sha512-7GlnT24gEwDrdAwEHrU4Vv5lLWrEer4KOkAiKT9nYstsTad7Oc1TwqT2zIMKRdZF7cTuaf+GA1E4Kv7jJh8mPA== + dependencies: + unist-util-visit "^2.0.0" + mdast-util-definitions@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-2.0.1.tgz#2c931d8665a96670639f17f98e32c3afcfee25f3" @@ -24219,6 +24238,26 @@ remark-squeeze-paragraphs@4.0.0: dependencies: mdast-squeeze-paragraphs "^4.0.0" +remark-stringify@^8.0.3: + version "8.1.1" + resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-8.1.1.tgz#e2a9dc7a7bf44e46a155ec78996db896780d8ce5" + integrity sha512-q4EyPZT3PcA3Eq7vPpT6bIdokXzFGp9i85igjmhRyXWmPs0Y6/d2FYwUNotKAWyLch7g0ASZJn/KHHcHZQ163A== + dependencies: + ccount "^1.0.0" + is-alphanumeric "^1.0.0" + is-decimal "^1.0.0" + is-whitespace-character "^1.0.0" + longest-streak "^2.0.1" + markdown-escapes "^1.0.0" + markdown-table "^2.0.0" + mdast-util-compact "^2.0.0" + parse-entities "^2.0.0" + repeat-string "^1.5.4" + state-toggle "^1.0.0" + stringify-entities "^3.0.0" + unherit "^1.0.4" + xtend "^4.0.1" + remark-stringify@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/remark-stringify/-/remark-stringify-9.0.1.tgz#576d06e910548b0a7191a71f27b33f1218862894" @@ -26150,7 +26189,7 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= -stringify-entities@^3.0.1: +stringify-entities@^3.0.0, stringify-entities@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-3.0.1.tgz#32154b91286ab0869ab2c07696223bd23b6dbfc0" integrity sha512-Lsk3ISA2++eJYqBMPKcr/8eby1I6L0gP0NlxF8Zja6c05yr/yCYyb2c9PwXjd08Ib3If1vn1rbs1H5ZtVuOfvQ==