diff --git a/src/keywordPreservation.ts b/src/keywordPreservation.ts index a98d73d10..95b1a0bde 100644 --- a/src/keywordPreservation.ts +++ b/src/keywordPreservation.ts @@ -1,6 +1,13 @@ import { get as getByDotNotation, set as setByDotNotation } from 'dot-prop'; +import { keywordReplace } from './tools/utils'; import { KeywordMappings } from './types'; import { keywordReplaceArrayRegExp, keywordReplaceStringRegExp } from './tools/utils'; +import { cloneDeep } from 'lodash'; + +/* + RFC for Keyword Preservation: https://github.com/auth0/auth0-deploy-cli/issues/688 + Original Github Issue: https://github.com/auth0/auth0-deploy-cli/issues/328 +*/ export const shouldFieldBePreserved = ( string: string, @@ -15,9 +22,9 @@ export const shouldFieldBePreserved = ( }; export const getPreservableFieldsFromAssets = ( - asset: any, - address: string, - keywordMappings: KeywordMappings + asset: object, + keywordMappings: KeywordMappings, + address = '' ): string[] => { if (typeof asset === 'string') { if (shouldFieldBePreserved(asset, keywordMappings)) { @@ -39,8 +46,8 @@ export const getPreservableFieldsFromAssets = ( return getPreservableFieldsFromAssets( arrayItem, - `${address}${shouldRenderDot ? '.' : ''}[name=${arrayItem.name}]`, - keywordMappings + keywordMappings, + `${address}${shouldRenderDot ? '.' : ''}[name=${arrayItem.name}]` ); }) .flat(); @@ -54,8 +61,8 @@ export const getPreservableFieldsFromAssets = ( return getPreservableFieldsFromAssets( value, - `${address}${shouldRenderDot ? '.' : ''}${key}`, - keywordMappings + keywordMappings, + `${address}${shouldRenderDot ? '.' : ''}${key}` ); }) .flat(); @@ -160,3 +167,40 @@ export const updateAssetsByAddress = ( setByDotNotation(assets, dotNotationAddress, newValue); return assets; }; + +// preserveKeywords is the function that ultimately gets executed during export +// to attempt to preserve keywords (ex: ##KEYWORD##) in local configuration files +// from getting overwritten by remote values during export. +export const preserveKeywords = ( + localAssets: object, + remoteAssets: object, + keywordMappings: KeywordMappings +): object => { + const addresses = getPreservableFieldsFromAssets(localAssets, keywordMappings, ''); + + let updatedRemoteAssets = cloneDeep(remoteAssets); + + addresses.forEach((address) => { + const localValue = getAssetsValueByAddress(address, localAssets); + const remoteValue = getAssetsValueByAddress(address, remoteAssets); + + const localValueWithReplacement = keywordReplace(localValue, keywordMappings); + + const localAndRemoteValuesAreEqual = (() => { + if (typeof remoteValue === 'string') { + return localValueWithReplacement === remoteValue; + } + //TODO: Account for non-string replacements via @@ syntax + })(); + + if (!localAndRemoteValuesAreEqual) { + console.warn( + `WARNING! The remote value with address of ${address} has value of "${remoteValue}" but will be preserved with "${localValueWithReplacement}" due to keyword preservation.` + ); + } + + updatedRemoteAssets = updateAssetsByAddress(updatedRemoteAssets, address, localValue); + }); + + return updatedRemoteAssets; +}; diff --git a/test/keywordPreservation.test.ts b/test/keywordPreservation.test.ts index eb1e169f2..63b3991e8 100644 --- a/test/keywordPreservation.test.ts +++ b/test/keywordPreservation.test.ts @@ -6,7 +6,9 @@ import { getAssetsValueByAddress, convertAddressToDotNotation, updateAssetsByAddress, + preserveKeywords, } from '../src/keywordPreservation'; +import { cloneDeep } from 'lodash'; describe('#Keyword Preservation', () => { describe('shouldFieldBePreserved', () => { @@ -82,7 +84,6 @@ describe('#Keyword Preservation', () => { nullField: null, undefinedField: undefined, }, - '', { KEYWORD: 'Travel0', ARRAY_REPLACE_KEYWORD: ['this value', 'that value'], @@ -277,3 +278,70 @@ describe('updateAssetsByAddress', () => { ); }); }); + +describe('preserveKeywords', () => { + const mockLocalAssets = { + tenant: { + display_name: 'The ##COMPANY_NAME## Tenant', + allowed_logout_urls: '@@ALLOWED_LOGOUT_URLS@@', + }, + roles: null, + hooks: undefined, + actions: [ + { + name: 'action-1', + display_name: '##ENV## Action 1', + }, + { + name: 'action-2', + display_name: "This action won't exist on remote, will be deleted", + }, + ], + }; + + const mockRemoteAssets = { + tenant: { + display_name: 'The Travel0 Tenant', + allowed_logout_urls: ['localhost:3000/logout', 'https://travel0.com/logout'], + }, + prompts: { + universal_login_enabled: true, + customText: {}, + }, + pages: undefined, //TODO: test these cases more thoroughly + rules: null, //TODO: test these cases more thoroughly + actions: [ + { + name: 'action-1', + display_name: 'Production Action 1', + }, + { + name: 'action-3', + display_name: 'This action exists on remote but not local', + }, + ], + }; + + it('should preserve keywords when they correlate to keyword mappings', () => { + const preservedAssets = preserveKeywords(mockLocalAssets, mockRemoteAssets, { + COMPANY_NAME: 'Travel0', + ALLOWED_LOGOUT_URLS: ['localhost:3000/logout', 'https://travel0.com/logout'], + ENV: 'Production', + }); + + expect(preservedAssets).to.deep.equal( + (() => { + const expected = cloneDeep(mockRemoteAssets); + //@ts-ignore + expected.tenant = mockLocalAssets.tenant; + expected.actions[0].display_name = '##ENV## Action 1'; + return expected; + })() + ); + }); + + it('should not preserve keywords when no keyword mappings', () => { + const preservedAssets = preserveKeywords(mockLocalAssets, mockRemoteAssets, {}); + expect(preservedAssets).to.deep.equal(mockRemoteAssets); + }); +});