From 7d6f71fe9bfa1e40f95f33f92556e1a864fccbc9 Mon Sep 17 00:00:00 2001 From: Jesus Cabrera Date: Tue, 13 Oct 2020 12:17:47 -0400 Subject: [PATCH] Update typescript example to pass contract tests --- typescript/.rpdk-config | 3 +- typescript/brianterry-unicorn-maker.json | 5 +- typescript/package-lock.json | 130 ++++++++++++++--------- typescript/package.json | 2 +- typescript/src/handlers.ts | 94 +++++++++++----- typescript/src/models.ts | 59 ++++++++-- typescript/template.yml | 3 +- 7 files changed, 208 insertions(+), 88 deletions(-) diff --git a/typescript/.rpdk-config b/typescript/.rpdk-config index e584f38..95c80a2 100644 --- a/typescript/.rpdk-config +++ b/typescript/.rpdk-config @@ -6,6 +6,7 @@ "testEntrypoint": "dist/handlers.testEntrypoint", "settings": { "useDocker": false, - "buildCommand": "npm run build && NPM_CONFIG_OPTIONAL=0 sam build --build-dir ./build --debug" + "buildCommand": "npm run build && NPM_CONFIG_OPTIONAL=0 sam build --build-dir ./build --debug", + "protocolVersion": "2.0.0" } } diff --git a/typescript/brianterry-unicorn-maker.json b/typescript/brianterry-unicorn-maker.json index b4a0d7d..9308b77 100644 --- a/typescript/brianterry-unicorn-maker.json +++ b/typescript/brianterry-unicorn-maker.json @@ -5,17 +5,20 @@ "properties": { "UID": { "description": "The ID of the majestic animal", - "type": "string" + "type": "string", + "pattern": "^[a-z0-9]+$" }, "Name": { "description": "The name of the majestic animal", "type": "string", + "pattern": "^[a-zA-Z0-9]+$", "minLength": 3, "maxLength": 250 }, "Color": { "description": "The Color of the majestic animal", "type": "string", + "pattern": "^[a-zA-Z0-9]+$", "minLength": 3, "maxLength": 250 } diff --git a/typescript/package-lock.json b/typescript/package-lock.json index 24a2716..de667a1 100644 --- a/typescript/package-lock.json +++ b/typescript/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@types/node": { - "version": "12.12.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.37.tgz", - "integrity": "sha512-4mXKoDptrXAwZErQHrLzpe0FN/0Wmf5JRniSVIdwUrtDf9wnmEV1teCNLBo/TwuXhkK/bVegoEn/wmb+x0AuPg==", + "version": "12.12.62", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.62.tgz", + "integrity": "sha512-qAfo81CsD7yQIM9mVyh6B/U47li5g7cfpVQEDMfQeF8pSZVwzbhwU3crc0qG4DmpsebpJPR49AKOExQyJ05Cpg==", "dev": true }, "autobind-decorator": { @@ -16,12 +16,12 @@ "integrity": "sha512-OGYhWUO72V6DafbF8PM8rm3EPbfuyMZcJhtm5/n26IDwO18pohE4eNazLoCGhPiXOCD0gEGmrbU3849QvM8bbw==" }, "aws-sdk": { - "version": "2.665.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.665.0.tgz", - "integrity": "sha512-1V5b6yWgRTXFHtt0yB8Lg6nGTnqQ1EJ5KsbrQ2grpmEZsz7Rm0J/jIFdv3VUDcHqJOrFPKLC3L7a6FE36y0leg==", + "version": "2.765.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.765.0.tgz", + "integrity": "sha512-FQdPKJ5LAhNxkpqwrjQ+hiEqEOezV/PfZBn5RcBG6vu8K3VuT4dE12mGTY3qkdHy7lhymeRS5rWcagTmabKFtA==", "optional": true, "requires": { - "buffer": "4.9.1", + "buffer": "4.9.2", "events": "1.1.1", "ieee754": "1.1.13", "jmespath": "0.15.0", @@ -30,14 +30,6 @@ "url": "0.10.3", "uuid": "3.3.2", "xml2js": "0.4.19" - }, - "dependencies": { - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "optional": true - } } }, "base64-js": { @@ -47,9 +39,9 @@ "optional": true }, "buffer": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", - "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "optional": true, "requires": { "base64-js": "^1.0.2", @@ -57,29 +49,60 @@ "isarray": "^1.0.0" } }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, "cfn-rpdk": { - "version": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin/releases/download/v0.1.0/cfn-rpdk-0.1.0.tgz", - "integrity": "sha512-VZMvB/8N5xyopW6ee0UD8hhblltyqVvXQt4GGmDnEdon+cOCrU/7e1bBdKYXO5Lvq5rJw0iypzw5cVu7sOs4KQ==", + "version": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin/releases/download/v0.3.3/cfn-rpdk-0.3.3.tgz", + "integrity": "sha512-JroVTA75Z+z7MbYcBso7pHFX0nLzZisVeSmhmyp9/17P7t3AJoR4XVgF40NP4WhRRFiBWq05rkCzcPvxSUFe0Q==", "requires": { "autobind-decorator": "^2.4.0", - "aws-sdk": "^2.656.0", + "aws-sdk": "~2.712.0", + "class-transformer": "^0.3.1", + "promise-sequential": "^1.1.1", "reflect-metadata": "^0.1.13", "tombok": "https://github.com/eduardomourar/tombok/releases/download/v0.0.1/tombok-0.0.1.tgz", "uuid": "^7.0.2" + }, + "dependencies": { + "aws-sdk": { + "version": "2.712.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.712.0.tgz", + "integrity": "sha512-C3SLWanFydoWJwtKNi73BG9uB6UzrUuECaAiplOEVBltO/R4sBsHWhwTBuxS02eTNdRrgulu19bJ5RWt+OuXiA==", + "optional": true, + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "optional": true + } + } + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + } } }, + "class-transformer": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.3.1.tgz", + "integrity": "sha512-cKFwohpJbuMovS8xVLmn8N2AUbAuc8pVo4zEfsUVo8qgECOogns1WVk/FkOZoxhOPTyTYFckuoH+13FO+MQ8GA==" + }, "data-uri-to-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.0.tgz", - "integrity": "sha512-MJ6mFTZ+nPQO+39ua/ltwNePXrfdF3Ww0wP1Od7EePySXN1cP9XNqRQOG3FxTfipp8jx898LUCgBCEP11Qw/ZQ==", - "requires": { - "buffer-from": "^1.1.1" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", + "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" }, "events": { "version": "1.1.1", @@ -88,15 +111,14 @@ "optional": true }, "fetch-blob": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-1.0.5.tgz", - "integrity": "sha512-BIggzO037jmCrZmtgntzCD2ymEaWgw9OMJsfX7FOS1jXGqKW9FEhETJN8QK4KxzIJknRl3RQdyzz34of+NNTMQ==" + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.1.tgz", + "integrity": "sha512-Uf+gxPCe1hTOFXwkxYyckn8iUSk6CFXGy5VENZKifovUTZC9eUODWSBhOBS7zICGrAetKzdwLMr85KhIcePMAQ==" }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "optional": true + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, "isarray": { "version": "1.0.0", @@ -111,14 +133,19 @@ "optional": true }, "node-fetch": { - "version": "3.0.0-beta.5", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.5.tgz", - "integrity": "sha512-PAQuLryEp/5tuLI3IBQk5Sn2NicJdpdNIdSF1vuTeT4pwYrv9Iwq70RK5WewDYeci2U2VEcAY8LXiMk+MUFdAw==", + "version": "3.0.0-beta.9", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", + "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", "requires": { - "data-uri-to-buffer": "^3.0.0", - "fetch-blob": "^1.0.5" + "data-uri-to-buffer": "^3.0.1", + "fetch-blob": "^2.1.1" } }, + "promise-sequential": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-sequential/-/promise-sequential-1.1.1.tgz", + "integrity": "sha1-956JUO+G56eoW/MgRSZDWS9tL7I=" + }, "punycode": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", @@ -128,8 +155,7 @@ "querystring": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "optional": true + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" }, "reflect-metadata": { "version": "0.1.13", @@ -139,17 +165,16 @@ "sax": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=", - "optional": true + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "tombok": { "version": "https://github.com/eduardomourar/tombok/releases/download/v0.0.1/tombok-0.0.1.tgz", "integrity": "sha512-JCo74uhjiMExAkuqIatwK96/ZZMrtlsVfgaZ3cWYY/IlPf3PWtzogwYMggGeuZf01qIBtFaPiGgfarpRiyN4uQ==" }, "typescript": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", - "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "url": { @@ -163,9 +188,10 @@ } }, "uuid": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", - "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==" + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "optional": true }, "xml2js": { "version": "0.4.19", diff --git a/typescript/package.json b/typescript/package.json index 5ccec07..4050904 100644 --- a/typescript/package.json +++ b/typescript/package.json @@ -27,7 +27,7 @@ "npm": ">=6.0.0" }, "dependencies": { - "cfn-rpdk": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin/releases/download/v0.1.0/cfn-rpdk-0.1.0.tgz", + "cfn-rpdk": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin/releases/download/v0.3.3/cfn-rpdk-0.3.3.tgz", "node-fetch": "^3.0.0-beta.4" }, "devDependencies": { diff --git a/typescript/src/handlers.ts b/typescript/src/handlers.ts index c459dc1..55dc61d 100644 --- a/typescript/src/handlers.ts +++ b/typescript/src/handlers.ts @@ -37,21 +37,31 @@ const checkedResponse = async (response: Response, uid?: string): Promise = return JSON.parse(data); } +interface CallbackContext extends Record {} + class Resource extends BaseResource { + /** + * CloudFormation invokes this handler when the resource is initially created + * during stack create operations. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to allow the passing through of additional + * state or metadata between subsequent retries + */ @handlerEvent(Action.Create) public async create( session: Optional, request: ResourceHandlerRequest, - callbackContext: Map, + callbackContext: CallbackContext, ): Promise { LOGGER.debug('CREATE request', request); const model: ResourceModel = request.desiredResourceState; - const progress: ProgressEvent = ProgressEvent.builder() - .status(OperationStatus.InProgress) - .resourceModel(model) - .build() as ProgressEvent; - const body: Object = model.toObject(); + if (model.UID) throw new exceptions.InvalidRequest("Create unicorn with readOnly property"); + + const progress = ProgressEvent.progress>(model); + const body: Object = { ...model }; LOGGER.debug('CREATE body', body); const response: Response = await fetch(API_ENDPOINT, { method: 'POST', @@ -61,23 +71,29 @@ class Resource extends BaseResource { const jsonData: any = await checkedResponse(response); progress.resourceModel.UID = jsonData['_id']; progress.status = OperationStatus.Success; - LOGGER.log('CREATE progress', progress.toObject()); + LOGGER.log('CREATE progress', { ...progress }); return progress; } + /** + * CloudFormation invokes this handler when the resource is updated + * as part of a stack update operation. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to allow the passing through of additional + * state or metadata between subsequent retries + */ @handlerEvent(Action.Update) public async update( session: Optional, request: ResourceHandlerRequest, - callbackContext: Map, + callbackContext: CallbackContext, ): Promise { LOGGER.debug('UPDATE request', request); const model: ResourceModel = request.desiredResourceState; - const progress: ProgressEvent = ProgressEvent.builder() - .status(OperationStatus.InProgress) - .resourceModel(model) - .build() as ProgressEvent; - const body: any = model.toObject(); + const progress = ProgressEvent.progress>(model); + const body: any = { ...model }; delete body['UID']; LOGGER.debug('UPDATE body', body); const response: Response = await fetch(`${API_ENDPOINT}/${model.UID}`, { @@ -87,37 +103,53 @@ class Resource extends BaseResource { }); await checkedResponse(response, model.UID); progress.status = OperationStatus.Success; - LOGGER.log('UPDATE progress', progress.toObject()); + LOGGER.log('UPDATE progress', { ...progress }); return progress; } + /** + * CloudFormation invokes this handler when the resource is deleted, either when + * the resource is deleted from the stack as part of a stack update operation, + * or the stack itself is deleted. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to allow the passing through of additional + * state or metadata between subsequent retries + */ @handlerEvent(Action.Delete) public async delete( session: Optional, request: ResourceHandlerRequest, - callbackContext: Map, + callbackContext: CallbackContext, ): Promise { LOGGER.debug('DELETE request', request); const model: ResourceModel = request.desiredResourceState; - const progress: ProgressEvent = ProgressEvent.builder() - .status(OperationStatus.InProgress) - .resourceModel(model) - .build() as ProgressEvent; + const progress = ProgressEvent.progress>(); const response: Response = await fetch(`${API_ENDPOINT}/${model.UID}`, { method: 'DELETE', headers: DEFAULT_HEADERS, }); await checkedResponse(response, model.UID); progress.status = OperationStatus.Success; - LOGGER.log('DELETE progress', progress.toObject()); + LOGGER.log('DELETE progress', { ...progress }); return progress; } + /** + * CloudFormation invokes this handler as part of a stack update operation when + * detailed information about the resource's current state is required. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to allow the passing through of additional + * state or metadata between subsequent retries + */ @handlerEvent(Action.Read) public async read( session: Optional, request: ResourceHandlerRequest, - callbackContext: Map, + callbackContext: CallbackContext, ): Promise { LOGGER.debug('READ request', request); const model: ResourceModel = request.desiredResourceState; @@ -132,15 +164,24 @@ class Resource extends BaseResource { .status(OperationStatus.Success) .resourceModel(model) .build() as ProgressEvent; - LOGGER.log('READ progress', progress.toObject()); + LOGGER.log('READ progress', { ...progress }); return progress; } + /** + * CloudFormation invokes this handler when summary information about multiple + * resources of this resource provider is required. + * + * @param session Current AWS session passed through from caller + * @param request The request object for the provisioning request passed to the implementor + * @param callbackContext Custom context object to allow the passing through of additional + * state or metadata between subsequent retries + */ @handlerEvent(Action.List) public async list( session: Optional, request: ResourceHandlerRequest, - callbackContext: Map, + callbackContext: CallbackContext, ): Promise { LOGGER.debug('LIST request', request); const response: Response = await fetch(API_ENDPOINT, { @@ -149,17 +190,18 @@ class Resource extends BaseResource { }); const jsonData: any[] = await checkedResponse(response); const models: Array = jsonData.map((unicorn: any) => { - return new ResourceModel(new Map(Object.entries({ + LOGGER.log("\n\nHERE", unicorn, "\n\n"); + return new ResourceModel({ UID: unicorn['_id'], Name: unicorn['Name'], Color: unicorn['Color'], - }))); + }); }); const progress: ProgressEvent = ProgressEvent.builder() .status(OperationStatus.Success) .resourceModels(models) .build() as ProgressEvent; - LOGGER.log('LIST progress', progress.toObject()); + LOGGER.log('LIST progress test logger', { ...progress }); return progress; } } diff --git a/typescript/src/models.ts b/typescript/src/models.ts index 5b5ed82..80a4317 100644 --- a/typescript/src/models.ts +++ b/typescript/src/models.ts @@ -1,12 +1,59 @@ // This is a generated file. Modifications will be overwritten. -import { BaseResourceModel, Optional } from 'cfn-rpdk'; +import { BaseModel, Dict, integer, Integer, Optional, transformValue } from 'cfn-rpdk'; +import { Exclude, Expose, Type, Transform } from 'class-transformer'; -export class ResourceModel extends BaseResourceModel { +export class ResourceModel extends BaseModel { ['constructor']: typeof ResourceModel; + + @Exclude() public static readonly TYPE_NAME: string = 'Brianterry::Unicorn::Maker'; - UID: Optional; - Name: Optional; - Color: Optional; -} + @Exclude() + protected readonly IDENTIFIER_KEY_UID: string = '/properties/UID'; + + @Expose({ name: 'UID' }) + @Transform( + (value: any, obj: any) => + transformValue(String, 'UID', value, obj, []), + { + toClassOnly: true, + } + ) + UID?: Optional; + @Expose({ name: 'Name' }) + @Transform( + (value: any, obj: any) => + transformValue(String, 'name', value, obj, []), + { + toClassOnly: true, + } + ) + Name?: Optional; + @Expose({ name: 'Color' }) + @Transform( + (value: any, obj: any) => + transformValue(String, 'color', value, obj, []), + { + toClassOnly: true, + } + ) + Color?: Optional; + @Exclude() + public getPrimaryIdentifier(): Dict { + const identifier: Dict = {}; + if (this.UID != null) { + identifier[this.IDENTIFIER_KEY_UID] = this.UID; + } + + // only return the identifier if it can be used, i.e. if all components are present + return Object.keys(identifier).length === 1 ? identifier : null; + } + + @Exclude() + public getAdditionalIdentifiers(): Array { + const identifiers: Array = new Array(); + // only return the identifiers if any can be used + return identifiers.length === 0 ? null : identifiers; + } +} diff --git a/typescript/template.yml b/typescript/template.yml index c078c11..dfc5346 100644 --- a/typescript/template.yml +++ b/typescript/template.yml @@ -4,7 +4,8 @@ Description: AWS SAM template for the Brianterry::Unicorn::Maker resource type Globals: Function: - Timeout: 60 # docker start-up times can be long for SAM CLI + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 1024 Resources: TypeFunction: