diff --git a/src/context/directory/index.ts b/src/context/directory/index.ts index 12f52104c..f679a7c5a 100644 --- a/src/context/directory/index.ts +++ b/src/context/directory/index.ts @@ -88,11 +88,12 @@ export default class DirectoryContext { //@ts-ignore delete this['assets']; - this.assets = preserveKeywords( + this.assets = preserveKeywords({ localAssets, - auth0.assets, - this.config.AUTH0_KEYWORD_REPLACE_MAPPINGS || {} - ); + remoteAssets: auth0.assets, + keywordMappings: this.config.AUTH0_KEYWORD_REPLACE_MAPPINGS || {}, + auth0Handlers: auth0.handlers, + }); } else { this.assets = auth0.assets; } diff --git a/src/context/yaml/index.ts b/src/context/yaml/index.ts index 210b29219..43c75d74c 100644 --- a/src/context/yaml/index.ts +++ b/src/context/yaml/index.ts @@ -132,11 +132,12 @@ export default class YAMLContext { //@ts-ignore delete this['assets']; - this.assets = preserveKeywords( + this.assets = preserveKeywords({ localAssets, - auth0.assets, - this.config.AUTH0_KEYWORD_REPLACE_MAPPINGS || {} - ); + remoteAssets: auth0.assets, + keywordMappings: this.config.AUTH0_KEYWORD_REPLACE_MAPPINGS || {}, + auth0Handlers: auth0.handlers, + }); } else { this.assets = auth0.assets; } diff --git a/src/keywordPreservation.ts b/src/keywordPreservation.ts index c482372e3..7547ade15 100644 --- a/src/keywordPreservation.ts +++ b/src/keywordPreservation.ts @@ -1,8 +1,9 @@ import { get as getByDotNotation, set as setByDotNotation } from 'dot-prop'; import { keywordReplace } from './tools/utils'; -import { KeywordMappings } from './types'; +import { AssetTypes, KeywordMappings } from './types'; import { keywordReplaceArrayRegExp, keywordReplaceStringRegExp } from './tools/utils'; import { cloneDeep } from 'lodash'; +import APIHandler from './tools/auth0/handlers/default'; /* RFC for Keyword Preservation: https://github.com/auth0/auth0-deploy-cli/issues/688 @@ -24,6 +25,7 @@ export const shouldFieldBePreserved = ( export const getPreservableFieldsFromAssets = ( asset: object, keywordMappings: KeywordMappings, + resourceSpecificIdentifiers: Partial<{ [key in AssetTypes]: string }>, address = '' ): string[] => { if (typeof asset === 'string') { @@ -38,16 +40,17 @@ export const getPreservableFieldsFromAssets = ( if (Array.isArray(asset)) { return asset .map((arrayItem) => { - // Using the `name` field as the primary unique identifier for array items - // TODO: expand the available identifier fields to encompass objects that lack name - const hasIdentifier = arrayItem.name !== undefined; + const resourceIdentifier = resourceSpecificIdentifiers[address]; + if (resourceIdentifier === undefined) return []; // See if this specific resource type has an identifier - if (!hasIdentifier) return []; + const identifierFieldValue = arrayItem[resourceIdentifier]; + if (identifierFieldValue === undefined) return []; // See if this specific array item possess the resource-specific identifier return getPreservableFieldsFromAssets( arrayItem, keywordMappings, - `${address}${shouldRenderDot ? '.' : ''}[name=${arrayItem.name}]` + resourceSpecificIdentifiers, + `${address}${shouldRenderDot ? '.' : ''}[${resourceIdentifier}=${identifierFieldValue}]` ); }) .flat(); @@ -62,6 +65,7 @@ export const getPreservableFieldsFromAssets = ( return getPreservableFieldsFromAssets( value, keywordMappings, + resourceSpecificIdentifiers, `${address}${shouldRenderDot ? '.' : ''}${key}` ); }) @@ -173,14 +177,37 @@ export const updateAssetsByAddress = ( // 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 => { +export const preserveKeywords = ({ + localAssets, + remoteAssets, + keywordMappings, + auth0Handlers, +}: { + localAssets: object; + remoteAssets: object; + keywordMappings: KeywordMappings; + auth0Handlers: Pick[]; +}): object => { if (Object.keys(keywordMappings).length === 0) return remoteAssets; - const addresses = getPreservableFieldsFromAssets(localAssets, keywordMappings, ''); + const resourceSpecificIdentifiers = auth0Handlers.reduce( + (acc, handler): Partial<{ [key in AssetTypes]: string }> => { + return { + ...acc, + [handler.type]: handler.identifiers.filter((identifiers) => { + return identifiers !== handler.id; + })[0], + }; + }, + {} + ); + + const addresses = getPreservableFieldsFromAssets( + localAssets, + keywordMappings, + resourceSpecificIdentifiers, + '' + ); let updatedRemoteAssets = cloneDeep(remoteAssets); diff --git a/test/e2e/e2e.test.ts b/test/e2e/e2e.test.ts index 656fa499f..1fbd24fcc 100644 --- a/test/e2e/e2e.test.ts +++ b/test/e2e/e2e.test.ts @@ -396,6 +396,10 @@ describe('keyword preservation', () => { expect(yaml.tenant.friendly_name).to.equal('##TENANT_NAME##'); expect(yaml.tenant.support_email).to.equal('support@##DOMAIN##'); expect(yaml.tenant.support_url).to.equal('https://##DOMAIN##/support'); + expect( + yaml.emailTemplates.find(({ template }) => template === 'welcome_email').resultUrl + ).to.equal('https://##DOMAIN##/welcome'); + // expect(yaml.tenant.enabled_locales).to.equal('@@LANGUAGES@@'); TODO: enable @@ARRAY@@ keyword preservation in yaml formats // const emailTemplateHTML = fs @@ -431,6 +435,13 @@ describe('keyword preservation', () => { expect(json.support_email).to.equal('support@##DOMAIN##'); expect(json.support_url).to.equal('https://##DOMAIN##/support'); + const emailTemplateJson = JSON.parse( + fs.readFileSync(path.join(workDirectory, 'emailTemplates', 'welcome_email.json')).toString() + ); + + expect(emailTemplateJson.resultUrl).to.equal('https://##DOMAIN##/welcome'); + expect(emailTemplateJson.subject).to.not.equal('##THIS_SHOULD_NOT_BE_PRESERVED##'); + // const emailTemplateHTML = fs // .readFileSync(path.join(workDirectory, 'emailTemplates', 'welcome_email.html')) // .toString(); diff --git a/test/e2e/testdata/should-preserve-keywords/directory/emailTemplates/welcome_email.json b/test/e2e/testdata/should-preserve-keywords/directory/emailTemplates/welcome_email.json new file mode 100644 index 000000000..6f917cd28 --- /dev/null +++ b/test/e2e/testdata/should-preserve-keywords/directory/emailTemplates/welcome_email.json @@ -0,0 +1,11 @@ +{ + "template": "welcome_email", + "body": "./welcome_email.html", + "from": "", + "resultUrl": "https://##DOMAIN##/welcome", + "subject": "Welcome", + "syntax": "liquid", + "urlLifetimeInSeconds": 3600, + "enabled": false + } + \ No newline at end of file diff --git a/test/keywordPreservation.test.ts b/test/keywordPreservation.test.ts index 933ee8869..3cbac8b12 100644 --- a/test/keywordPreservation.test.ts +++ b/test/keywordPreservation.test.ts @@ -52,7 +52,7 @@ describe('#Keyword Preservation', () => { it('should retrieve all preservable fields from assets tree', () => { const fieldsToPreserve = getPreservableFieldsFromAssets( { - object: { + tenant: { friendly_name: 'Friendly name ##KEYWORD##', notInKeywordMapping: '##NOT_IN_KEYWORD_MAPPING##', number: 5, @@ -60,24 +60,23 @@ describe('#Keyword Preservation', () => { nested: { nestedProperty: 'Nested property ##KEYWORD##', }, + nestedArray: [ + { + name: 'nested-array-item-1', + value: + "Even with ##KEYWORD##, this won't get preserved because this nested array item does not have a registered resource identifier", + }, + ], }, - array: [ + actions: [ { - name: 'array-item-1', - nestedArray: [ - { - name: 'nested-array-item-1', - value: 'Nested array value 1 ##KEYWORD##', - }, - { - name: 'nested-array-item-2', - value: 'Nested array value 2 ##KEYWORD##', - }, - ], + actionName: 'action-1', + value: 'Action 1 ##KEYWORD##', notInKeywordMapping: '##NOT_IN_KEYWORD_MAPPING##', - nested: { - nestedProperty: 'Another nested array property ##KEYWORD##', - }, + }, + { + actionName: 'action-2', + value: 'Action 2 ##KEYWORD##', }, ], arrayReplace: '@@ARRAY_REPLACE_KEYWORD@@', @@ -87,15 +86,15 @@ describe('#Keyword Preservation', () => { { KEYWORD: 'Travel0', ARRAY_REPLACE_KEYWORD: ['this value', 'that value'], - } + }, + { actions: 'actionName' } ); expect(fieldsToPreserve).to.have.members([ - 'object.friendly_name', - 'object.nested.nestedProperty', - 'array.[name=array-item-1].nestedArray.[name=nested-array-item-1].value', - 'array.[name=array-item-1].nestedArray.[name=nested-array-item-2].value', - 'array.[name=array-item-1].nested.nestedProperty', + 'tenant.friendly_name', + 'tenant.nested.nestedProperty', + 'actions.[actionName=action-1].value', + 'actions.[actionName=action-2].value', 'arrayReplace', ]); }); @@ -292,6 +291,12 @@ describe('preserveKeywords', () => { }, }, ], + emailTemplates: [ + { + template: 'welcome', + body: 'Welcome to ##ENV## ##COMPANY_NAME## Tenant', + }, + ], actions: [ { name: 'action-1', @@ -326,13 +331,42 @@ describe('preserveKeywords', () => { display_name: 'This action exists on remote but not local', }, ], + emailTemplates: [ + { + template: 'welcome', + body: 'Welcome to Production Travel0 Tenant', + }, + ], }; + const auth0Handlers = [ + { + id: 'id', + identifiers: ['id', 'name'], + type: 'actions', + }, + { + id: 'id', + identifiers: ['id', 'name'], + type: 'connections', + }, + { + id: 'id', + identifiers: ['template'], + type: 'emailTemplates', + }, + ]; + 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', + const preservedAssets = preserveKeywords({ + localAssets: mockLocalAssets, + remoteAssets: mockRemoteAssets, + keywordMappings: { + COMPANY_NAME: 'Travel0', + ALLOWED_LOGOUT_URLS: ['localhost:3000/logout', 'https://travel0.com/logout'], + ENV: 'Production', + }, + auth0Handlers, }); expect(preservedAssets).to.deep.equal( @@ -341,13 +375,19 @@ describe('preserveKeywords', () => { //@ts-ignore expected.tenant = mockLocalAssets.tenant; expected.actions[0].display_name = '##ENV## Action 1'; + expected.emailTemplates[0].body = 'Welcome to ##ENV## ##COMPANY_NAME## Tenant'; return expected; })() ); }); it('should not preserve keywords when no keyword mappings', () => { - const preservedAssets = preserveKeywords(mockLocalAssets, mockRemoteAssets, {}); + const preservedAssets = preserveKeywords({ + localAssets: mockLocalAssets, + remoteAssets: mockRemoteAssets, + keywordMappings: {}, + auth0Handlers, + }); expect(preservedAssets).to.deep.equal(mockRemoteAssets); }); });