Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions __tests__/commands/dev-env-sync-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,68 @@ describe( 'commands/DevEnvSyncSQLCommand', () => {
'test.go-vip.com/path': 'test-slug.vipdev.lndo.site/path',
} );
} );

it( 'should add user search-replace pairs to the map', () => {
const cmd = new DevEnvSyncSQLCommand( app, env, 'test-slug', lando, () => {}, undefined, [
'old.com,new.com',
'foo,bar',
] );
cmd.slug = 'test-slug';
cmd.siteUrls = [ 'http://test.go-vip.com' ];
cmd.generateSearchReplaceMap();

expect( cmd.searchReplaceMap ).toEqual( {
'test.go-vip.com': 'test-slug.vipdev.lndo.site',
'old.com': 'new.com',
foo: 'bar',
} );
} );

it( 'should allow comma in replace value', () => {
const cmd = new DevEnvSyncSQLCommand( app, env, 'test-slug', lando, () => {}, undefined, [
'old,new,value',
] );
cmd.slug = 'test-slug';
cmd.siteUrls = [];
cmd.generateSearchReplaceMap();

expect( cmd.searchReplaceMap ).toEqual( {
old: 'new,value',
} );
} );

it( 'should allow empty replace value', () => {
const cmd = new DevEnvSyncSQLCommand( app, env, 'test-slug', lando, () => {}, undefined, [
'old,',
] );
cmd.slug = 'test-slug';
cmd.siteUrls = [];
cmd.generateSearchReplaceMap();

expect( cmd.searchReplaceMap ).toEqual( {
old: '',
} );
} );

it( 'should throw error for invalid format', () => {
const cmd = new DevEnvSyncSQLCommand( app, env, 'test-slug', lando, () => {}, undefined, [
'invalidpair',
] );
cmd.slug = 'test-slug';
cmd.siteUrls = [];

expect( () => cmd.generateSearchReplaceMap() ).toThrow( 'Invalid search-replace format' );
} );

it( 'should throw error for empty search value', () => {
const cmd = new DevEnvSyncSQLCommand( app, env, 'test-slug', lando, () => {}, undefined, [
',replace',
] );
cmd.slug = 'test-slug';
cmd.siteUrls = [];

expect( () => cmd.generateSearchReplaceMap() ).toThrow( 'Search value cannot be empty' );
} );
} );

describe( '.runSearchReplace', () => {
Expand Down
22 changes: 20 additions & 2 deletions src/bin/vip-dev-env-sync-sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ const examples = [
description:
'Sync the entire database of the develop environment in the "example-app" application to a local environment named "example-site".',
},
{
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --search-replace="example.com,localhost"`,
description:
'Sync the database and perform additional search-replace operations on the SQL file during sync.',
},
{
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --search-replace="old.com,new.com" --search-replace="foo,bar"`,
description: 'Perform multiple search-replace operations during the database sync.',
},
{
usage: `vip @example-app.develop dev-env sync sql --slug=example-site --table=wp_posts --table=wp_comments`,
description:
Expand Down Expand Up @@ -101,10 +110,14 @@ command( {
'A local configuration file that specifies the data to include in the partial database sync. Accepts a relative or absolute path to the file.',
undefined
)
.option(
[ 'r', 'search-replace' ],
'Search for a string in the SQL file and replace it with a new string. Can be passed multiple times.'
)
.option( 'force', 'Skip validations.', undefined, processBooleanOption )
.examples( examples )
.argv( process.argv, async ( arg, opt ) => {
const { app, env, configFile, table, siteId, wpcliCommand, ...optRest } = opt;
const { app, env, configFile, table, siteId, wpcliCommand, searchReplace, ...optRest } = opt;

const liveBackupCopyCLIOptions = parseLiveBackupCopyCLIOptions(
configFile,
Expand All @@ -113,6 +126,10 @@ command( {
wpcliCommand
);

// Normalize searchReplace to an array
const normalizedSearchReplace =
searchReplace && ! Array.isArray( searchReplace ) ? [ searchReplace ] : searchReplace;

const slug = await getEnvironmentName( optRest );
const trackerFn = makeCommandTracker( 'dev_env_sync_sql', {
app: app.id,
Expand Down Expand Up @@ -142,7 +159,8 @@ command( {
slug,
lando,
trackerFn,
liveBackupCopyCLIOptions
liveBackupCopyCLIOptions,
normalizedSearchReplace
);
// TODO: There's a function called handleCLIException for dev-env that handles exceptions but DevEnvSyncSQLCommand has its own implementation.
// We should probably use handleCLIException instead?
Expand Down
82 changes: 58 additions & 24 deletions src/commands/dev-env-sync-sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class DevEnvSyncSQLCommand {
public _track: TrackFunction;
private _sqlDumpType?: SqlDumpType;
public sdsSiteUrls: WpSite[] = [];
private readonly userSearchReplacePairs?: string[];

/**
* Creates a new instance of the command
Expand All @@ -129,11 +130,13 @@ export class DevEnvSyncSQLCommand {
public slug: string,
public lando: Lando,
trackerFn: TrackFunction = () => {},
liveBackupCopyCLIOptions?: LiveBackupCopyCLIOptions
liveBackupCopyCLIOptions?: LiveBackupCopyCLIOptions,
userSearchReplace?: string[]
) {
this._track = trackerFn;
this.tmpDir = makeTempDir();
this.liveBackupCopyCLIOptions = liveBackupCopyCLIOptions;
this.userSearchReplacePairs = userSearchReplace;
}

public track( name: string, eventProps: Record< string, unknown > ) {
Expand Down Expand Up @@ -289,35 +292,66 @@ export class DevEnvSyncSQLCommand {
}

const networkSites = this.sdsSiteUrls;
if ( ! networkSites.length ) return;
if ( networkSites.length ) {
const primaryUrl = networkSites.find( site => site?.blogId === 1 )?.homeUrl;
const primaryDomain = primaryUrl ? new URL( primaryUrl ).hostname : '';
debug(
'Network sites: %j, primary URL: %s, primary domain: %s',
networkSites.map( site => ( { blogId: site?.blogId, homeUrl: site?.homeUrl } ) ),
primaryUrl,
primaryDomain
);

const primaryUrl = networkSites.find( site => site?.blogId === 1 )?.homeUrl;
const primaryDomain = primaryUrl ? new URL( primaryUrl ).hostname : '';
debug(
'Network sites: %j, primary URL: %s, primary domain: %s',
networkSites.map( site => ( { blogId: site?.blogId, homeUrl: site?.homeUrl } ) ),
primaryUrl,
primaryDomain
);
for ( const site of networkSites ) {
if ( ! site?.blogId || site.blogId === 1 ) continue;

for ( const site of networkSites ) {
if ( ! site?.blogId || site.blogId === 1 ) continue;
const url = site?.homeUrl;
if ( ! url ) continue;

const url = site?.homeUrl;
if ( ! url ) continue;
const strippedUrl = stripProtocol( url ).replace( /\/$/, '' );
if ( ! this.searchReplaceMap[ strippedUrl ] ) continue;

const strippedUrl = stripProtocol( url ).replace( /\/$/, '' );
if ( ! this.searchReplaceMap[ strippedUrl ] ) continue;
const domain = new URL( url ).hostname;
const newDomain =
primaryDomain === domain
? this.landoDomain
: `${ this.slugifyDomain( domain ) }.${ this.landoDomain }`;

const domain = new URL( url ).hostname;
const newDomain =
primaryDomain === domain
? this.landoDomain
: `${ this.slugifyDomain( domain ) }.${ this.landoDomain }`;
this.searchReplaceMap[ stripProtocol( url ) ] = stripProtocol(
replaceDomain( url, newDomain )
);
}
}

this.searchReplaceMap[ stripProtocol( url ) ] = stripProtocol(
replaceDomain( url, newDomain )
);
// Add user-provided search-replace pairs
this.addUserSearchReplacePairs();
}

/**
* Adds user-provided search-replace pairs to the searchReplaceMap
*/
private addUserSearchReplacePairs(): void {
if ( ! this.userSearchReplacePairs?.length ) {
return;
}

for ( const pair of this.userSearchReplacePairs ) {
// Split on first comma only, so replace can contain commas
const commaIndex = pair.indexOf( ',' );
if ( commaIndex === -1 ) {
throw new Error(
`Invalid search-replace format: "${ pair }". Expected format: "search,replace"`
);
}

const search = pair.substring( 0, commaIndex ).trim();
const replaceValue = pair.substring( commaIndex + 1 ).trim();

if ( ! search ) {
throw new Error( 'Search value cannot be empty' );
}

this.searchReplaceMap[ search ] = replaceValue;
}
}

Expand Down
Loading