-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(resolvers): Implement substitutions for js resolvers (#545)
- Loading branch information
Showing
5 changed files
with
241 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import { Api } from '../resources/Api'; | ||
import { JsResolver } from '../resources/JsResolver'; | ||
import * as given from './given'; | ||
import fs from 'fs'; | ||
|
||
const plugin = given.plugin(); | ||
|
||
describe('Mapping Templates', () => { | ||
let mock: jest.SpyInstance; | ||
let mockEists: jest.SpyInstance; | ||
|
||
beforeEach(() => { | ||
mock = jest | ||
.spyOn(fs, 'readFileSync') | ||
.mockImplementation( | ||
(path) => `Content of ${`${path}`.replace(/\\/g, '/')}`, | ||
); | ||
mockEists = jest.spyOn(fs, 'existsSync').mockReturnValue(true); | ||
}); | ||
|
||
afterEach(() => { | ||
mock.mockRestore(); | ||
mockEists.mockRestore(); | ||
}); | ||
|
||
it('should substitute variables', () => { | ||
const api = new Api(given.appSyncConfig(), plugin); | ||
const mapping = new JsResolver(api, { | ||
path: 'foo.vtl', | ||
substitutions: { | ||
foo: 'bar', | ||
var: { Ref: 'MyReference' }, | ||
}, | ||
}); | ||
const template = `const foo = '#foo#'; | ||
const var = '#var#'; | ||
const unknonw = '#unknown#'`; | ||
expect(mapping.processTemplateSubstitutions(template)) | ||
.toMatchInlineSnapshot(` | ||
Object { | ||
"Fn::Join": Array [ | ||
"", | ||
Array [ | ||
"const foo = '", | ||
Object { | ||
"Fn::Sub": Array [ | ||
"\${foo}", | ||
Object { | ||
"foo": "bar", | ||
}, | ||
], | ||
}, | ||
"'; | ||
const var = '", | ||
Object { | ||
"Fn::Sub": Array [ | ||
"\${var}", | ||
Object { | ||
"var": Object { | ||
"Ref": "MyReference", | ||
}, | ||
}, | ||
], | ||
}, | ||
"'; | ||
const unknonw = '#unknown#'", | ||
], | ||
], | ||
} | ||
`); | ||
}); | ||
|
||
it('should substitute variables and use defaults', () => { | ||
const api = new Api( | ||
given.appSyncConfig({ | ||
substitutions: { | ||
foo: 'bar', | ||
var: 'bizz', | ||
}, | ||
}), | ||
plugin, | ||
); | ||
const mapping = new JsResolver(api, { | ||
path: 'foo.vtl', | ||
substitutions: { | ||
foo: 'fuzz', | ||
}, | ||
}); | ||
const template = `const foo = '#foo#'; | ||
const var = '#var#';`; | ||
expect(mapping.processTemplateSubstitutions(template)) | ||
.toMatchInlineSnapshot(` | ||
Object { | ||
"Fn::Join": Array [ | ||
"", | ||
Array [ | ||
"const foo = '", | ||
Object { | ||
"Fn::Sub": Array [ | ||
"\${foo}", | ||
Object { | ||
"foo": "fuzz", | ||
}, | ||
], | ||
}, | ||
"'; | ||
const var = '", | ||
Object { | ||
"Fn::Sub": Array [ | ||
"\${var}", | ||
Object { | ||
"var": "bizz", | ||
}, | ||
], | ||
}, | ||
"';", | ||
], | ||
], | ||
} | ||
`); | ||
}); | ||
|
||
it('should fail if template is missing', () => { | ||
mockEists = jest.spyOn(fs, 'existsSync').mockReturnValue(false); | ||
const api = new Api(given.appSyncConfig(), plugin); | ||
const mapping = new JsResolver(api, { | ||
path: 'foo.vtl', | ||
substitutions: { | ||
foo: 'bar', | ||
var: { Ref: 'MyReference' }, | ||
}, | ||
}); | ||
|
||
expect(function () { | ||
mapping.compile(); | ||
}).toThrowErrorMatchingInlineSnapshot( | ||
`"The resolver handler file 'foo.vtl' does not exist"`, | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
import { IntrinsicFunction } from '../types/cloudFormation'; | ||
import fs from 'fs'; | ||
import { Substitutions } from '../types/plugin'; | ||
import { Api } from './Api'; | ||
|
||
type JsResolverConfig = { | ||
path: string; | ||
substitutions?: Substitutions; | ||
}; | ||
|
||
export class JsResolver { | ||
constructor(private api: Api, private config: JsResolverConfig) {} | ||
|
||
compile(): string | IntrinsicFunction { | ||
if (!fs.existsSync(this.config.path)) { | ||
throw new this.api.plugin.serverless.classes.Error( | ||
`The resolver handler file '${this.config.path}' does not exist`, | ||
); | ||
} | ||
|
||
const requestTemplateContent = fs.readFileSync(this.config.path, 'utf8'); | ||
return this.processTemplateSubstitutions(requestTemplateContent); | ||
} | ||
|
||
processTemplateSubstitutions(template: string): string | IntrinsicFunction { | ||
const substitutions = { | ||
...this.api.config.substitutions, | ||
...this.config.substitutions, | ||
}; | ||
const availableVariables = Object.keys(substitutions); | ||
const templateVariables: string[] = []; | ||
let searchResult; | ||
const variableSyntax = RegExp(/#([\w\d-_]+)#/g); | ||
while ((searchResult = variableSyntax.exec(template)) !== null) { | ||
templateVariables.push(searchResult[1]); | ||
} | ||
|
||
const replacements = availableVariables | ||
.filter((value) => templateVariables.includes(value)) | ||
.filter((value, index, array) => array.indexOf(value) === index) | ||
.reduce( | ||
(accum, value) => | ||
Object.assign(accum, { [value]: substitutions[value] }), | ||
{}, | ||
); | ||
|
||
// if there are substitutions for this template then add fn:sub | ||
if (Object.keys(replacements).length > 0) { | ||
return this.substituteGlobalTemplateVariables(template, replacements); | ||
} | ||
|
||
return template; | ||
} | ||
|
||
/** | ||
* Creates Fn::Join object from given template where all given substitutions | ||
* are wrapped in Fn::Sub objects. This enables template to have also | ||
* characters that are not only alphanumeric, underscores, periods, and colons. | ||
* | ||
* @param {*} template | ||
* @param {*} substitutions | ||
*/ | ||
substituteGlobalTemplateVariables( | ||
template: string, | ||
substitutions: Substitutions, | ||
): IntrinsicFunction { | ||
const variables = Object.keys(substitutions).join('|'); | ||
const regex = new RegExp(`\\#(${variables})#`, 'g'); | ||
const substituteTemplate = template.replace(regex, '|||$1|||'); | ||
|
||
const templateJoin = substituteTemplate | ||
.split('|||') | ||
.filter((part) => part !== ''); | ||
const parts: (string | IntrinsicFunction)[] = []; | ||
for (let i = 0; i < templateJoin.length; i += 1) { | ||
if (templateJoin[i] in substitutions) { | ||
const subs = { [templateJoin[i]]: substitutions[templateJoin[i]] }; | ||
parts[i] = { 'Fn::Sub': [`\${${templateJoin[i]}}`, subs] }; | ||
} else { | ||
parts[i] = templateJoin[i]; | ||
} | ||
} | ||
return { 'Fn::Join': ['', parts] }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters