[Cases] Fix remark stringify version to match remark parse (#119995) (#120843)

* 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 <jonathan.buttner@elastic.co>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>

Co-authored-by: Sergi Massaneda <sergi.massaneda@elastic.co>
Co-authored-by: Jonathan Buttner <jonathan.buttner@elastic.co>
This commit is contained in:
Kibana Machine 2021-12-08 18:03:45 -05:00 committed by GitHub
parent 74f651f132
commit 61e5a692e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 555 additions and 166 deletions

View file

@ -365,7 +365,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",
"require-in-the-middle": "^5.1.0",
"reselect": "^4.0.0",
"resize-observer-polyfill": "^1.5.1",

View file

@ -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;
};

View file

@ -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;
};

View file

@ -18,5 +18,93 @@ describe('markdown utils', () => {
const parsed = parseCommentString('hello\n');
expect(stringifyMarkdownComment(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(stringifyMarkdownComment(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(stringifyMarkdownComment(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(stringifyMarkdownComment(parsedNodes)).toEqual(`${lensVisualization}\n`);
});
});
});

View file

@ -45,20 +45,13 @@ export const parseCommentString = (comment: string) => {
export const stringifyMarkdownComment = (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);

View file

@ -5,7 +5,12 @@
* 2.0.
*/
import { createCommentsMigrations, stringifyCommentWithoutTrailingNewline } from './comments';
import {
createCommentsMigrations,
mergeMigrationFunctionMaps,
migrateByValueLensVisualizations,
stringifyCommentWithoutTrailingNewline,
} from './comments';
import {
getLensVisualizations,
parseCommentString,
@ -14,84 +19,98 @@ import {
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';
import {
SavedObjectReference,
SavedObjectsMigrationLogger,
SavedObjectUnsanitizedDoc,
} from 'kibana/server';
import {
MigrateFunction,
MigrateFunctionsObject,
} from '../../../../../../src/plugins/kibana_utils/common';
import { SerializableRecord } from '@kbn/utility-types';
const migrations = createCommentsMigrations({
lensEmbeddableFactory,
});
describe('comments migrations', () => {
const migrations = createCommentsMigrations({
lensEmbeddableFactory,
});
const contextMock = savedObjectsServiceMock.createMigrationContext();
describe('index migrations', () => {
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: {},
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: [],
},
};
},
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: '',
@ -241,43 +260,140 @@ describe('index migrations', () => {
expect((columns[2] as { params: {} }).params).toEqual({ timeZone: 'do not delete' });
});
});
});
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);
describe('handles errors', () => {
interface CommentSerializable extends SerializableRecord {
comment?: string;
}
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome'
);
const migrationFunction: MigrateFunction<CommentSerializable, CommentSerializable> = (
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<SavedObjectsMigrationLogger>;
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<SavedObjectsMigrationLogger>;
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('leaves the newline if it was in the original comment', () => {
const originalComment = 'awesome\n';
const parsedString = parseCommentString(originalComment);
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;
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome\n'
);
});
const migrationObj2 = {
'2.0.0': (doc: SavedObjectUnsanitizedDoc<{ comment?: string }>) => {
throw new Error('2.0.0 error');
},
};
it('does not remove newlines that are not at the end of the comment', () => {
const originalComment = 'awesome\ncomment';
const parsedString = parseCommentString(originalComment);
const mergedFunctions = mergeMigrationFunctionMaps(migrationObj1, migrationObj2);
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome\ncomment'
);
});
expect(() => mergedFunctions['2.0.0'](caseComment, contextMock)).toThrow();
it('does not remove spaces at the end of the comment', () => {
const originalComment = 'awesome ';
const parsedString = parseCommentString(originalComment);
expect(stringifyCommentWithoutTrailingNewline(originalComment, parsedString)).toEqual(
'awesome '
);
const log = contextMock.log as jest.Mocked<SavedObjectsMigrationLogger>;
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 '
);
});
});
});

View file

@ -5,12 +5,9 @@
* 2.0.
*/
import { mapValues, trimEnd } from 'lodash';
import { SerializableRecord } from '@kbn/utility-types';
import { LensServerPluginSetup } from '../../../../lens/server';
import { mapValues, trimEnd, mergeWith } from 'lodash';
import type { SerializableRecord } from '@kbn/utility-types';
import {
mergeMigrationFunctionMaps,
MigrateFunction,
MigrateFunctionsObject,
} from '../../../../../../src/plugins/kibana_utils/common';
@ -19,7 +16,9 @@ import {
SavedObjectSanitizedDoc,
SavedObjectMigrationFn,
SavedObjectMigrationMap,
SavedObjectMigrationContext,
} from '../../../../../../src/core/server';
import { LensServerPluginSetup } from '../../../../lens/server';
import { CommentType, AssociationType } from '../../../common/api';
import {
isLensMarkdownNode,
@ -29,6 +28,7 @@ import {
stringifyMarkdownComment,
} from '../../../common/utils/markdown_plugins/utils';
import { addOwnerToSO, SanitizedCaseOwner } from '.';
import { logError } from './utils';
interface UnsanitizedComment {
comment: string;
@ -103,33 +103,41 @@ export const createCommentsMigrations = (
return mergeMigrationFunctionMaps(commentsMigrations, embeddableMigrations);
};
const migrateByValueLensVisualizations =
(migrate: MigrateFunction, version: string): SavedObjectMigrationFn<{ comment?: string }> =>
(doc: SavedObjectUnsanitizedDoc<{ comment?: string }>) => {
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;
}
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;
}
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;
});
return comment;
});
const migratedMarkdown = { ...parsedComment, children: migratedComment };
const migratedMarkdown = { ...parsedComment, children: migratedComment };
return {
...doc,
attributes: {
...doc.attributes,
comment: stringifyCommentWithoutTrailingNewline(doc.attributes.comment, migratedMarkdown),
},
};
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 = (
@ -147,3 +155,23 @@ export const stringifyCommentWithoutTrailingNewline = (
// 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);
};

View file

@ -7,7 +7,11 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { SavedObjectMigrationContext, SavedObjectSanitizedDoc } from 'kibana/server';
import {
SavedObjectMigrationContext,
SavedObjectSanitizedDoc,
SavedObjectsMigrationLogger,
} from 'kibana/server';
import { migrationMocks } from 'src/core/server/mocks';
import { CaseUserActionAttributes } from '../../../common/api';
import { CASE_USER_ACTION_SAVED_OBJECT } from '../../../common/constants';
@ -217,7 +221,19 @@ describe('user action migrations', () => {
userActionsConnectorIdMigration(userAction, context);
expect(context.log.error).toHaveBeenCalled();
const log = context.log as jest.Mocked<SavedObjectsMigrationLogger>;
expect(log.error.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"Failed to migrate user action connector with doc id: 1 version: 8.0.0 error: Unexpected token a in JSON at position 1",
Object {
"migrations": Object {
"userAction": Object {
"id": "1",
},
},
},
]
`);
});
});
@ -385,7 +401,19 @@ describe('user action migrations', () => {
userActionsConnectorIdMigration(userAction, context);
expect(context.log.error).toHaveBeenCalled();
const log = context.log as jest.Mocked<SavedObjectsMigrationLogger>;
expect(log.error.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"Failed to migrate user action connector with doc id: 1 version: 8.0.0 error: Unexpected token b in JSON at position 1",
Object {
"migrations": Object {
"userAction": Object {
"id": "1",
},
},
},
]
`);
});
});
@ -555,7 +583,19 @@ describe('user action migrations', () => {
userActionsConnectorIdMigration(userAction, context);
expect(context.log.error).toHaveBeenCalled();
const log = context.log as jest.Mocked<SavedObjectsMigrationLogger>;
expect(log.error.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"Failed to migrate user action connector with doc id: 1 version: 8.0.0 error: Unexpected token e in JSON at position 1",
Object {
"migrations": Object {
"userAction": Object {
"id": "1",
},
},
},
]
`);
});
});
});

View file

@ -12,13 +12,13 @@ import {
SavedObjectUnsanitizedDoc,
SavedObjectSanitizedDoc,
SavedObjectMigrationContext,
LogMeta,
} from '../../../../../../src/core/server';
import { isPush, isUpdateConnector, isCreateConnector } from '../../../common/utils/user_actions';
import { ConnectorTypes } from '../../../common/api';
import { extractConnectorIdFromJson } from '../../services/user_actions/transform';
import { UserActionFieldType } from '../../services/user_actions/types';
import { logError } from './utils';
interface UserActions {
action_field: string[];
@ -33,10 +33,6 @@ interface UserActionUnmigratedConnectorDocument {
old_value?: string | null;
}
interface UserActionLogMeta extends LogMeta {
migrations: { userAction: { id: string } };
}
export function userActionsConnectorIdMigration(
doc: SavedObjectUnsanitizedDoc<UserActionUnmigratedConnectorDocument>,
context: SavedObjectMigrationContext
@ -50,7 +46,13 @@ export function userActionsConnectorIdMigration(
try {
return formatDocumentWithConnectorReferences(doc);
} catch (error) {
logError(doc.id, context, error);
logError({
id: doc.id,
context,
error,
docType: 'user action connector',
docKey: 'userAction',
});
return originalDocWithReferences;
}
@ -99,19 +101,6 @@ function formatDocumentWithConnectorReferences(
};
}
function logError(id: string, context: SavedObjectMigrationContext, error: Error) {
context.log.error<UserActionLogMeta>(
`Failed to migrate user action connector doc id: ${id} version: ${context.migrationVersion} error: ${error.message}`,
{
migrations: {
userAction: {
id,
},
},
}
);
}
export const userActionsMigrations = {
'7.10.0': (doc: SavedObjectUnsanitizedDoc<UserActions>): SavedObjectSanitizedDoc<UserActions> => {
const { action_field, new_value, old_value, ...restAttributes } = doc.attributes;

View file

@ -0,0 +1,43 @@
/*
* 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 { SavedObjectsMigrationLogger } from 'kibana/server';
import { migrationMocks } from '../../../../../../src/core/server/mocks';
import { logError } from './utils';
describe('migration utils', () => {
const context = migrationMocks.createContext();
beforeEach(() => {
jest.clearAllMocks();
});
it('logs an error', () => {
const log = context.log as jest.Mocked<SavedObjectsMigrationLogger>;
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",
},
},
},
]
`);
});
});

View file

@ -0,0 +1,41 @@
/*
* 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 { LogMeta, SavedObjectMigrationContext } from '../../../../../../src/core/server';
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<MigrationLogMeta>(
`Failed to migrate ${docType} with doc id: ${id} version: ${context.migrationVersion} error: ${error.message}`,
{
migrations: {
[docKey]: {
id,
},
},
}
);
}

View file

@ -16357,6 +16357,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"
@ -18935,7 +18940,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==
@ -19193,6 +19198,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"
@ -19265,6 +19277,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@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-4.0.0.tgz#c5c1a84db799173b4dcf7643cda999e440c24db2"
@ -24351,6 +24370,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"
@ -26347,7 +26386,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==