UNPKG

typed-serverless

Version:

Helps you write a consistent Serverless Framework configuration in TypeScript

278 lines (277 loc) 11.7 kB
"use strict"; 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;