typed-serverless
Version:
Helps you write a consistent Serverless Framework configuration in TypeScript
278 lines (277 loc) • 11.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TypedServerless = void 0;
const placeholders_1 = require("./placeholders");
const replaceValue_1 = require("../utils/replaceValue");
const logger_1 = require("../utils/logger");
const traverseObject_1 = require("../utils/traverseObject");
const isCfIntrinsicFunction_1 = require("../utils/isCfIntrinsicFunction");
const serverlessNaming_1 = require("./serverlessNaming");
const arn_1 = require("./arn");
const defaults_1 = require("./defaults");
class TypedServerless {
constructor(params) {
this.params = params;
}
static createDefault() {
return new TypedServerless((0, defaults_1.defaultTypedServerlessParams)());
}
static create(params) {
return new TypedServerless(params);
}
createResourcePlaceholder(id, type, builder) {
return this.asPlaceholder(new placeholders_1.ServerlessResourcePlaceholder(id, type, builder));
}
extendsWith(extension) {
const newInstance = Object.create(this);
Object.assign(newInstance, extension(this));
return newInstance;
}
onlyFactory() {
return (object) => object;
}
only(object) {
return object;
}
addResources(resources) {
return Object.keys(resources).reduce((out, id) => {
out[id] = this.createResourcePlaceholder(id, 'resource', resources[id]);
return out;
}, {});
}
resources(resources) {
return this.addResources(resources);
}
resource(resource) {
return this.resources(resource);
}
functions(functions) {
return Object.keys(functions).reduce((out, id) => {
out[id] = this.createResourcePlaceholder(id, 'function', functions[id]);
return out;
}, {});
}
asPlaceholder(placeholder) {
return placeholder;
}
refId(id) {
return new placeholders_1.GetResourceLogicalId(id);
}
ref(id) {
return this.asPlaceholder(new placeholders_1.CfRef(id));
}
getRef(id) {
return this.ref(id);
}
arn(id) {
return new placeholders_1.CfRefAtt(id, 'Arn');
}
getArn(id) {
return this.arn(id);
}
getAtt(id, attribute) {
return new placeholders_1.CfRefAtt(id, attribute);
}
getName(id) {
return new placeholders_1.GetResourceName(id);
}
fnSub(content, params) {
if (!params)
return { 'Fn::Sub': content };
return { 'Fn::Sub': [content, params] };
}
buildLambdaArn(id) {
return new placeholders_1.BuildArn((0, arn_1.lambdaArn)(id));
}
buildBucketArn(id, path) {
return new placeholders_1.BuildArn((0, arn_1.bucketArn)(id, path));
}
buildSnsArn(id) {
return new placeholders_1.BuildArn((0, arn_1.snsArn)(id));
}
buildEventBusArn(id) {
return new placeholders_1.BuildArn((0, arn_1.eventBusArn)(id));
}
buildSqsArn(id) {
return new placeholders_1.BuildArn((0, arn_1.sqsArn)(id));
}
/**
* @deprecated Prefer #arn - AWS Step Function automatically adds a name suffix, because of that its not possible to build a correct Arn
*/
buildStepFunctionArn(id) {
return new placeholders_1.BuildArn((0, arn_1.stepFunctionArn)(id));
}
buildAlarmArn(id) {
return new placeholders_1.BuildArn((0, arn_1.alarmArn)(id));
}
buildArn(id, params) {
return new placeholders_1.BuildArn({ ...params, resourceId: id });
}
/**
* The main use case for this is to overcome a limitation in CloudFormation that
* does not allow using intrinsic functions as dictionary keys (because
* dictionary keys in JSON must be strings). Specifically this is common in IAM
* conditions such as `StringEquals: { lhs: "rhs" }` where you want "lhs" to be
* a reference.
*/
stringify(content) {
return new placeholders_1.CfStringify(content);
}
cfn(expression) {
return expression;
}
resourcePlaceholderProcessor({ config, resourceNames, resourceTypes, }) {
// deep traverse our config to find and resource placeholders
(0, traverseObject_1.traverseObject)(config, (node, parent, key, path) => {
if (node instanceof placeholders_1.ServerlessResourcePlaceholder) {
const { id, type, builder } = node;
(0, logger_1.debug)('Registering resource', id);
const params = this.params.resourceParamsFactory(id, config);
(0, logger_1.trace)('Creating', type, id, 'parameters:', params);
// Register this resource name and type
resourceNames[id] = params.name;
resourceTypes[id] = type;
// Invoke builder to create new data for this placeholder
const object = builder(params);
(0, logger_1.trace)('Created', type, id, 'object:', object);
// Replace placeholder with new data
(0, replaceValue_1.replaceValue)(parent, key, path, object);
if (type === 'resource') {
this.params?.onResourceCreated?.(object);
}
else if (type === 'function') {
this.params?.onFunctionCreated?.(object);
}
// stop visiting child properties, we do not support nested resources
return false;
}
return true;
});
}
requiresResource(targetId, sourcePath, { errors, resourceNames, resourceTypes }) {
// validate if it's pointing to a registered resource...
const name = resourceNames[targetId];
if (!name) {
const message = `Referenced resource '${targetId}' not found! Check your configuration at '${sourcePath.join('.')}'`;
(0, logger_1.error)(message);
errors.push(message);
return null;
}
const logicalId = resourceTypes[targetId] === 'function'
? (0, serverlessNaming_1.getServerlessAwsFunctionLogicalId)(targetId)
: targetId;
return { logicalId, name };
}
buildArnPlaceholderProcessor(processContext) {
// deep traverse our config to find and replace placeholders
(0, traverseObject_1.traverseObject)(processContext.config, (node, parent, key, path) => {
// if its a reference replaceholder...
if (node instanceof placeholders_1.BuildArn) {
const id = node.params.resourceId;
const resource = this.requiresResource(id, path, processContext);
if (!resource)
return true;
// replace our placeholder with a real content...
const arn = (0, arn_1.buildArnFnSub)({
...node.params,
resourceId: resource.name,
});
(0, replaceValue_1.replaceValue)(parent, key, path, arn);
}
// continue visiting all child nodes
return true;
});
}
referencePlaceholderProcessor(processContext) {
// deep traverse our config to find and replace placeholders
(0, traverseObject_1.traverseObject)(processContext.config, (node, parent, key, path) => {
// if its a reference replaceholder...
if (node instanceof placeholders_1.GetResourceName ||
node instanceof placeholders_1.CfRefAtt ||
node instanceof placeholders_1.CfRef ||
node instanceof placeholders_1.GetResourceLogicalId) {
const resource = this.requiresResource(node.id, path, processContext);
if (!resource)
return true;
// replace our placeholder with a real content...
if (node instanceof placeholders_1.GetResourceName) {
(0, replaceValue_1.replaceValue)(parent, key, path, resource.name);
}
else if (node instanceof placeholders_1.CfRefAtt) {
const data = { 'Fn::GetAtt': [resource.logicalId, node.attribute] };
(0, replaceValue_1.replaceValue)(parent, key, path, data);
}
else if (node instanceof placeholders_1.CfRef) {
const data = { Ref: resource.logicalId };
(0, replaceValue_1.replaceValue)(parent, key, path, data);
}
else if (node instanceof placeholders_1.GetResourceLogicalId) {
(0, replaceValue_1.replaceValue)(parent, key, path, resource.logicalId);
}
}
// continue visiting all child nodes
return true;
});
}
replaceStringifyPlaceholders({ config, }) {
// deep traverse our config to find and replace placeholders
(0, traverseObject_1.traverseObject)(config, (node, parent, key, path) => {
if (node instanceof placeholders_1.CfStringify) {
// extract cloudformation expressions as parameters
const extractedParams = {};
// traverse all CloudFormation expressions Fn::* or Ref
(0, traverseObject_1.traverseObject)(node, (childNode, parent, key, path) => {
if ((0, isCfIntrinsicFunction_1.isCfIntrinsicFunction)(childNode)) {
const paramName = `extracted_param_${Object.keys(extractedParams).length}`;
extractedParams[paramName] = childNode;
(0, replaceValue_1.replaceValue)(parent, key, path, '${' + paramName + '}');
return false;
}
return true;
});
// Stringify content and replace with Fn::Sub [content, extractedParams]
(0, replaceValue_1.replaceValue)(parent, key, path, {
'Fn::Sub': [JSON.stringify(node.content), extractedParams],
});
}
return true;
});
}
processHook(hookPhase, processContext) {
this.params.hooks?.[hookPhase]?.(processContext);
}
processPlaceholders(processContext) {
// Replace Resource Placeholders
this.processHook('before-resource', processContext);
this.resourcePlaceholderProcessor(processContext);
this.processHook('after-resource', processContext);
// Replace BuildArn Placeholders
this.buildArnPlaceholderProcessor(processContext);
// Replace Reference Placeholders
this.processHook('before-reference', processContext);
this.referencePlaceholderProcessor(processContext);
this.processHook('after-reference', processContext);
// Replace Stringify Placeholders
this.processHook('before-stringify', processContext);
this.replaceStringifyPlaceholders(processContext);
this.processHook('after-stringify', processContext);
}
process(config) {
const processContext = {
config,
errors: [],
resourceNames: {},
resourceTypes: {},
};
this.processPlaceholders(processContext);
return processContext;
}
build(rawConfig) {
const { config, errors } = this.process(rawConfig);
if (errors.length) {
throw Object.assign(new Error(`Validation errors!\n\t${errors.join('\n\t')}`), { errors, config });
}
return config;
}
}
exports.TypedServerless = TypedServerless;