import { set } from '@elastic/safer-lodash-set' ; import _ from 'lodash' ; import { SavedObjectUnsanitizedDoc } from '../../serialization' ; import { DocumentMigrator } from './document_migrator' ; import { loggingSystemMock } from '../../../logging/logging_system.mock' ; import { SavedObjectsType } from '../../types' ; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry' ; const mockLoggerFactory = loggingSystemMock . create ( ) ; const mockLogger = mockLoggerFactory . get ( 'mock logger' ) ; const createRegistry = ( ... types ) => { const registry = new SavedObjectTypeRegistry ( ) ; types . forEach ( ( type ) => registry . registerType ( { name : 'unknown' , namespaceType : 'single' , hidden : false , mappings : { properties : { } } , migrations : { } , ... type , } ) ) ; return registry ; } ; describe ( 'DocumentMigrator' , ( ) => { function testOpts ( ) { return { opensearchDashboardsVersion : '25.2.3' , typeRegistry : createRegistry ( ) , log : mockLogger , } ; } it ( 'validates individual migration definitions' , ( ) => { const invalidDefinition = { opensearchDashboardsVersion : '3.2.3' , typeRegistry : createRegistry ( { name : 'foo' , migrations : _ . noop as any , } ) , log : mockLogger , } ; expect ( ( ) => new DocumentMigrator ( invalidDefinition ) ) . toThrow ( /Migration for type foo should be an object/i ) ; } ) ; it ( 'validates individual migration semvers' , ( ) => { const invalidDefinition = { opensearchDashboardsVersion : '3.2.3' , typeRegistry : createRegistry ( { name : 'foo' , migrations : { bar : ( doc ) => doc , } , } ) , log : mockLogger , } ; expect ( ( ) => new DocumentMigrator ( invalidDefinition ) ) . toThrow ( /Expected all properties to be semvers/i ) ; } ) ; it ( 'validates the migration function' , ( ) => { const invalidDefinition = { opensearchDashboardsVersion : '3.2.3' , typeRegistry : createRegistry ( { name : 'foo' , migrations : { '1.2.3' : 23 as any , } , } ) , log : mockLogger , } ; expect ( ( ) => new DocumentMigrator ( invalidDefinition ) ) . toThrow ( /expected a function, but got 23/i ) ; } ) ; it ( 'migrates type and attributes' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'user' , migrations : { '1.2.3' : setAttr ( 'attributes.name' , 'Chris' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'me' , type : 'user' , attributes : { name : 'Christopher' } , migrationVersion : { } , } ) ; expect ( actual ) . toEqual ( { id : 'me' , type : 'user' , attributes : { name : 'Chris' } , migrationVersion : { user : '1.2.3' } , } ) ; } ) ; it ( `doesn't mutate the original document` , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'user' , migrations : { '1.2.3' : ( doc ) => { set ( doc , 'attributes.name' , 'Mike' ) ; return doc ; } , } , } ) , } ) ; const originalDoc = { id : 'me' , type : 'user' , attributes : { } , migrationVersion : { } , } ; const migratedDoc = migrator . migrate ( originalDoc ) ; expect ( _ . get ( originalDoc , 'attributes.name' ) ) . toBeUndefined ( ) ; expect ( _ . get ( migratedDoc , 'attributes.name' ) ) . toBe ( 'Mike' ) ; } ) ; it ( 'migrates root properties' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'acl' , migrations : { '2.3.5' : setAttr ( 'acl' , 'admins-only, sucka!' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'me' , type : 'user' , attributes : { name : 'Tyler' } , acl : 'anyone' , migrationVersion : { } , } as SavedObjectUnsanitizedDoc ) ; expect ( actual ) . toEqual ( { id : 'me' , type : 'user' , attributes : { name : 'Tyler' } , migrationVersion : { acl : '2.3.5' } , acl : 'admins-only, sucka!' , } ) ; } ) ; it ( 'does not apply migrations to unrelated docs' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'aaa' , migrations : { '1.0.0' : setAttr ( 'aaa' , 'A' ) , } , } , { name : 'bbb' , migrations : { '1.0.0' : setAttr ( 'bbb' , 'B' ) , } , } , { name : 'ccc' , migrations : { '1.0.0' : setAttr ( 'ccc' , 'C' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'me' , type : 'user' , attributes : { name : 'Tyler' } , migrationVersion : { } , } ) ; expect ( actual ) . toEqual ( { id : 'me' , type : 'user' , attributes : { name : 'Tyler' } , } ) ; } ) ; it ( 'assumes documents w/ undefined migrationVersion are up to date' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'user' , migrations : { '1.0.0' : setAttr ( 'aaa' , 'A' ) , } , } , { name : 'bbb' , migrations : { '2.3.4' : setAttr ( 'bbb' , 'B' ) , } , } , { name : 'ccc' , migrations : { '1.0.0' : setAttr ( 'ccc' , 'C' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'me' , type : 'user' , attributes : { name : 'Tyler' } , bbb : 'Shazm' , } as SavedObjectUnsanitizedDoc ) ; expect ( actual ) . toEqual ( { id : 'me' , type : 'user' , attributes : { name : 'Tyler' } , bbb : 'Shazm' , migrationVersion : { user : '1.0.0' , bbb : '2.3.4' , } , } ) ; } ) ; it ( 'only applies migrations that are more recent than the doc' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'dog' , migrations : { '1.2.3' : setAttr ( 'attributes.a' , 'A' ) , '1.2.4' : setAttr ( 'attributes.b' , 'B' ) , '2.0.1' : setAttr ( 'attributes.c' , 'C' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' } , migrationVersion : { dog : '1.2.3' } , } ) ; expect ( actual ) . toEqual ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' , b : 'B' , c : 'C' } , migrationVersion : { dog : '2.0.1' } , } ) ; } ) ; it ( 'rejects docs that belong to a newer OpenSearch Dashboards instance' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , opensearchDashboardsVersion : '8.0.1' , } ) ; expect ( ( ) => migrator . migrate ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' } , migrationVersion : { dog : '10.2.0' } , } ) ) . toThrow ( /Document "smelly" has property "dog" which belongs to a more recent version of OpenSearch Dashboards \[10\.2\.0\]\. The last known version is \[undefined\]/i ) ; } ) ; it ( 'rejects docs that belong to a newer plugin' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'dawg' , migrations : { '1.2.3' : setAttr ( 'attributes.a' , 'A' ) , } , } ) , } ) ; expect ( ( ) => migrator . migrate ( { id : 'fleabag' , type : 'dawg' , attributes : { name : 'Callie' } , migrationVersion : { dawg : '1.2.4' } , } ) ) . toThrow ( /Document "fleabag" has property "dawg" which belongs to a more recent version of OpenSearch Dashboards \[1\.2\.4\]\. The last known version is \[1\.2\.3\]/i ) ; } ) ; it ( 'applies migrations in order' , ( ) => { let count = 0 ; const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'dog' , migrations : { '2.2.4' : setAttr ( 'attributes.b' , ( ) => ++ count ) , '10.0.1' : setAttr ( 'attributes.c' , ( ) => ++ count ) , '1.2.3' : setAttr ( 'attributes.a' , ( ) => ++ count ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' } , migrationVersion : { dog : '1.2.0' } , } ) ; expect ( actual ) . toEqual ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' , a : 1 , b : 2 , c : 3 } , migrationVersion : { dog : '10.0.1' } , } ) ; } ) ; it ( 'allows props to be added' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'animal' , migrations : { '1.0.0' : setAttr ( 'animal' , ( name ) => `Animal: ${ name }` ) , } , } , { name : 'dog' , migrations : { '2.2.4' : setAttr ( 'animal' , 'Doggie' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' } , migrationVersion : { dog : '1.2.0' } , } ) ; expect ( actual ) . toEqual ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' } , animal : 'Animal: Doggie' , migrationVersion : { animal : '1.0.0' , dog : '2.2.4' } , } ) ; } ) ; it ( 'allows props to be renamed' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'animal' , migrations : { '1.0.0' : setAttr ( 'animal' , ( name ) => `Animal: ${ name }` ) , '3.2.1' : renameAttr ( 'animal' , 'dawg' ) , } , } , { name : 'dawg' , migrations : { '2.2.4' : renameAttr ( 'dawg' , 'animal' ) , '3.2.0' : setAttr ( 'dawg' , ( name ) => `Dawg3.x: ${ name }` ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'smelly' , type : 'foo' , attributes : { name : 'Callie' } , dawg : 'Yo' , migrationVersion : { } , } as SavedObjectUnsanitizedDoc ) ; expect ( actual ) . toEqual ( { id : 'smelly' , type : 'foo' , attributes : { name : 'Callie' } , dawg : 'Dawg3.x: Animal: Yo' , migrationVersion : { animal : '3.2.1' , dawg : '3.2.0' } , } ) ; } ) ; it ( 'allows changing type' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'cat' , migrations : { '1.0.0' : setAttr ( 'attributes.name' , ( name ) => `Kitty ${ name }` ) , } , } , { name : 'dog' , migrations : { '2.2.4' : setAttr ( 'type' , 'cat' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'smelly' , type : 'dog' , attributes : { name : 'Callie' } , migrationVersion : { } , } ) ; expect ( actual ) . toEqual ( { id : 'smelly' , type : 'cat' , attributes : { name : 'Kitty Callie' } , migrationVersion : { dog : '2.2.4' , cat : '1.0.0' } , } ) ; } ) ; it ( 'disallows updating a migrationVersion prop to a lower version' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'cat' , migrations : { '1.0.0' : setAttr ( 'migrationVersion.foo' , '3.2.1' ) , } , } ) , } ) ; expect ( ( ) => migrator . migrate ( { id : 'smelly' , type : 'cat' , attributes : { name : 'Boo' } , migrationVersion : { foo : '4.5.6' } , } ) ) . toThrow ( /Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to 3.2.1./ ) ; } ) ; it ( 'disallows removing a migrationVersion prop' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'cat' , migrations : { '1.0.0' : setAttr ( 'migrationVersion' , { } ) , } , } ) , } ) ; expect ( ( ) => migrator . migrate ( { id : 'smelly' , type : 'cat' , attributes : { name : 'Boo' } , migrationVersion : { foo : '4.5.6' } , } ) ) . toThrow ( /Migration "cat v 1.0.0" attempted to downgrade "migrationVersion.foo" from 4.5.6 to undefined./ ) ; } ) ; it ( 'allows updating a migrationVersion prop to a later version' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'cat' , migrations : { '1.0.0' : setAttr ( 'migrationVersion.cat' , '2.9.1' ) , '2.0.0' : ( ) => { throw new Error ( 'POW!' ) ; } , '2.9.1' : ( ) => { throw new Error ( 'BANG!' ) ; } , '3.0.0' : setAttr ( 'attributes.name' , 'Shiny' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'smelly' , type : 'cat' , attributes : { name : 'Boo' } , migrationVersion : { cat : '0.5.6' } , } ) ; expect ( actual ) . toEqual ( { id : 'smelly' , type : 'cat' , attributes : { name : 'Shiny' } , migrationVersion : { cat : '3.0.0' } , } ) ; } ) ; it ( 'allows adding props to migrationVersion' , ( ) => { const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'cat' , migrations : { '1.0.0' : setAttr ( 'migrationVersion.foo' , '5.6.7' ) , } , } ) , } ) ; const actual = migrator . migrate ( { id : 'smelly' , type : 'cat' , attributes : { name : 'Boo' } , migrationVersion : { } , } ) ; expect ( actual ) . toEqual ( { id : 'smelly' , type : 'cat' , attributes : { name : 'Boo' } , migrationVersion : { cat : '1.0.0' , foo : '5.6.7' } , } ) ; } ) ; it ( 'logs the document and transform that failed' , ( ) => { const log = mockLogger ; const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'dog' , migrations : { '1.2.3' : ( ) => { throw new Error ( 'Dang diggity!' ) ; } , } , } ) , log , } ) ; const failedDoc = { id : 'smelly' , type : 'dog' , attributes : { } , migrationVersion : { } , } ; try { migrator . migrate ( _ . cloneDeep ( failedDoc ) ) ; expect ( 'Did not throw' ) . toEqual ( 'But it should have!' ) ; } catch ( error ) { expect ( error . message ) . toMatch ( /Dang diggity!/ ) ; const warning = loggingSystemMock . collect ( mockLoggerFactory ) . warn [ 0 ] [ 0 ] ; expect ( warning ) . toContain ( JSON . stringify ( failedDoc ) ) ; expect ( warning ) . toContain ( 'dog:1.2.3' ) ; } } ) ; it ( 'logs message in transform function' , ( ) => { const logTestMsg = '...said the joker to the thief' ; const migrator = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'dog' , migrations : { '1.2.3' : ( doc , { log } ) => { log . info ( logTestMsg ) ; log . warning ( logTestMsg ) ; return doc ; } , } , } ) , log : mockLogger , } ) ; const doc = { id : 'joker' , type : 'dog' , attributes : { } , migrationVersion : { } , } ; migrator . migrate ( doc ) ; expect ( loggingSystemMock . collect ( mockLoggerFactory ) . info [ 0 ] [ 0 ] ) . toEqual ( logTestMsg ) ; expect ( loggingSystemMock . collect ( mockLoggerFactory ) . warn [ 1 ] [ 0 ] ) . toEqual ( logTestMsg ) ; } ) ; test ( 'extracts the latest migration version info' , ( ) => { const { migrationVersion } = new DocumentMigrator ( { ... testOpts ( ) , typeRegistry : createRegistry ( { name : 'aaa' , migrations : { '1.2.3' : ( doc ) => doc , '10.4.0' : ( doc ) => doc , '2.2.1' : ( doc ) => doc , } , } , { name : 'bbb' , migrations : { '3.2.3' : ( doc ) => doc , '2.0.0' : ( doc ) => doc , } , } ) , } ) ; expect ( migrationVersion ) . toEqual ( { aaa : '10.4.0' , bbb : '3.2.3' , } ) ; } ) ; } ) ; function renameAttr ( path , newPath ) { return ( doc ) => _ . omit ( set ( doc , newPath , _ . get ( doc , path ) ) as { } , path ) as SavedObjectUnsanitizedDoc ; } function setAttr ( path , value ) { return ( doc ) => set ( doc , path , _ . isFunction ( value ) ? value ( _ . get ( doc , path ) ) : value ) as SavedObjectUnsanitizedDoc ; }