From ce487dccfe8ce1b3bec8efaad6e89a2b50f2d0d3 Mon Sep 17 00:00:00 2001 From: Duncan Grant Date: Wed, 29 Jun 2022 14:39:54 +0100 Subject: [PATCH 01/11] Add type configuration support to the typescript cli plugin --- .pre-commit-config.yaml | 4 +- README.md | 6 +- python/rpdk/typescript/codegen.py | 10 ++ python/rpdk/typescript/templates/handlers.ts | 4 +- src/exceptions.ts | 9 ++ src/interface.ts | 2 + src/resource.ts | 53 ++++++-- tests/lib/resource.test.ts | 128 ++++++++++++++----- 8 files changed, 164 insertions(+), 52 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f355909..32b8831 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,8 +7,8 @@ repos: - repo: https://github.com/ambv/black rev: stable hooks: - - id: black - # language_version: python3.6 + - id: black + # language_version: python3.6 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.0.0 hooks: diff --git a/README.md b/README.md index 33a2733..d231bd5 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ If you are using this package to build resource providers for CloudFormation, in **Prerequisites** - - Python version 3.6 or above - - [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) - - Your choice of TypeScript IDE +- Python version 3.6 or above +- [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) +- Your choice of TypeScript IDE **Installation** diff --git a/python/rpdk/typescript/codegen.py b/python/rpdk/typescript/codegen.py index b23c7ae..59e29c6 100644 --- a/python/rpdk/typescript/codegen.py +++ b/python/rpdk/typescript/codegen.py @@ -161,6 +161,15 @@ def generate(self, project): models = resolve_models(project.schema) + if project.configuration_schema: + configuration_models = resolve_models( + project.configuration_schema, "TypeConfigurationModel" + ) + else: + configuration_models = {"TypeConfigurationModel": {}} + + models.update(configuration_models) + path = self.package_root / "models.ts" LOG.debug("Writing file: %s", path) template = self.env.get_template("models.ts") @@ -168,6 +177,7 @@ def generate(self, project): lib_name=SUPPORT_LIB_NAME, type_name=project.type_name, models=models, + contains_type_configuration=project.configuration_schema, primaryIdentifier=project.schema.get("primaryIdentifier", []), additionalIdentifiers=project.schema.get("additionalIdentifiers", []), ) diff --git a/python/rpdk/typescript/templates/handlers.ts b/python/rpdk/typescript/templates/handlers.ts index 60300c3..3cb0d9b 100644 --- a/python/rpdk/typescript/templates/handlers.ts +++ b/python/rpdk/typescript/templates/handlers.ts @@ -11,7 +11,7 @@ import { ResourceHandlerRequest, SessionProxy, } from '{{lib_name}}'; -import { ResourceModel } from './models'; +import { ResourceModel, TypeConfigurationModel } from './models'; interface CallbackContext extends Record {} @@ -154,7 +154,7 @@ class Resource extends BaseResource { } } -export const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel); +export const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel, TypeConfigurationModel); // Entrypoint for production usage after registered in CloudFormation export const entrypoint = resource.entrypoint; diff --git a/src/exceptions.ts b/src/exceptions.ts index 1c776af..d8536ec 100644 --- a/src/exceptions.ts +++ b/src/exceptions.ts @@ -22,6 +22,15 @@ export class NotUpdatable extends BaseHandlerException {} export class InvalidRequest extends BaseHandlerException {} +export class InvalidTypeConfiguration extends BaseHandlerException { + constructor(typeName: string, reason: string) { + super( + `Invalid TypeConfiguration provided for type '${typeName}'. Reason: ${reason}`, + HandlerErrorCode.InvalidTypeConfiguration + ); + } +} + export class AccessDenied extends BaseHandlerException {} export class InvalidCredentials extends BaseHandlerException {} diff --git a/src/interface.ts b/src/interface.ts index 7833115..8b97307 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -160,6 +160,7 @@ export enum HandlerErrorCode { ServiceInternalError = 'ServiceInternalError', NetworkFailure = 'NetworkFailure', InternalFailure = 'InternalFailure', + InvalidTypeConfiguration = 'InvalidTypeConfiguration', } export interface Credentials { @@ -261,6 +262,7 @@ export class RequestData extends BaseDto { @Expose() providerCredentials?: Credentials; @Expose() previousResourceProperties?: T; @Expose() previousStackTags?: Dict; + @Expose() typeConfiguration?: Dict; } export class HandlerRequest extends BaseDto { diff --git a/src/resource.ts b/src/resource.ts index c56792a..e692a79 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -2,7 +2,12 @@ import 'reflect-metadata'; import { boundMethod } from 'autobind-decorator'; import { AwsTaskWorkerPool, ProgressEvent, SessionProxy } from './proxy'; -import { BaseHandlerException, InternalFailure, InvalidRequest } from './exceptions'; +import { + BaseHandlerException, + InternalFailure, + InvalidRequest, + InvalidTypeConfiguration, +} from './exceptions'; import { Action, BaseModel, @@ -40,13 +45,13 @@ const MUTATING_ACTIONS: [Action, Action, Action] = [ Action.Delete, ]; -export type HandlerSignature = Callable< - [Optional, any, Dict, LoggerProxy], +export type HandlerSignature = Callable< + [Optional, any, Dict, TC, LoggerProxy], Promise> >; -export class HandlerSignatures extends Map< +export class HandlerSignatures extends Map< Action, - HandlerSignature + HandlerSignature > {} class HandlerEvents extends Map {} @@ -88,7 +93,10 @@ function ensureSerialize(toResponse = false): MethodDecorat }; } -export abstract class BaseResource { +export abstract class BaseResource< + T extends BaseModel = BaseModel, + TC extends BaseModel = BaseModel +> { protected loggerProxy: LoggerProxy; protected metricsPublisherProxy: MetricsPublisherProxy; @@ -111,11 +119,14 @@ export abstract class BaseResource { constructor( public readonly typeName: string, public readonly modelTypeReference: Constructor, + public readonly typeConfigurationTypeReference: Constructor & { + deserialize: Function; + }, protected readonly workerPool?: AwsTaskWorkerPool, - private handlers?: HandlerSignatures + private handlers?: HandlerSignatures ) { this.typeName = typeName || ''; - this.handlers = handlers || new HandlerSignatures(); + this.handlers = handlers || new HandlerSignatures(); this.lambdaLogger = console; this.platformLoggerProxy = new LoggerProxy(); @@ -294,8 +305,8 @@ export abstract class BaseResource { public addHandler = ( action: Action, - f: HandlerSignature - ): HandlerSignature => { + f: HandlerSignature + ): HandlerSignature => { this.handlers.set(action, f); return f; }; @@ -304,13 +315,14 @@ export abstract class BaseResource { session: Optional, request: BaseResourceHandlerRequest, action: Action, - callbackContext: Dict + callbackContext: Dict, + typeConfiguration?: TC ): Promise> => { const actionName = action == null ? '' : action.toString(); if (!this.handlers.has(action)) { throw new Error(`Unknown action ${actionName}`); } - const handleRequest: HandlerSignature = this.handlers.get(action); + const handleRequest: HandlerSignature = this.handlers.get(action); // We will make the callback context and resource states readonly // to avoid modification at a later time deepFreeze(callbackContext); @@ -320,6 +332,7 @@ export abstract class BaseResource { session, request, callbackContext, + typeConfiguration, this.loggerProxy || this.platformLoggerProxy ); this.log(`[${action}] handler invoked`); @@ -473,6 +486,17 @@ export abstract class BaseResource { } }; + private castTypeConfigurationRequest = (request: HandlerRequest): TC => { + try { + return this.typeConfigurationTypeReference.deserialize( + request.requestData.typeConfiguration + ); + } catch (err) { + this.log('Invalid Type Configuration'); + throw new InvalidTypeConfiguration(this.typeName, `${err} (${err.name}`); + } + }; + // @ts-ignore public async entrypoint( eventData: any | Dict, @@ -500,6 +524,8 @@ export abstract class BaseResource { const [callerCredentials, providerCredentials] = credentials; const request = this.castResourceRequest(event); + const typeConfiguration = this.castTypeConfigurationRequest(event); + let streamName = `${event.awsAccountId}-${event.region}`; if (event.stackId && request.logicalResourceIdentifier) { streamName = `${event.stackId}/${request.logicalResourceIdentifier}`; @@ -550,7 +576,8 @@ export abstract class BaseResource { this.callerSession, request, action, - callback + callback, + typeConfiguration ); } catch (err) { error = err; diff --git a/tests/lib/resource.test.ts b/tests/lib/resource.test.ts index 45a73d1..607fadc 100644 --- a/tests/lib/resource.test.ts +++ b/tests/lib/resource.test.ts @@ -4,6 +4,7 @@ import * as exceptions from '~/exceptions'; import { ProgressEvent, SessionProxy } from '~/proxy'; import { Action, + BaseModel, BaseResourceHandlerRequest, HandlerErrorCode, HandlerRequest, @@ -43,7 +44,12 @@ describe('when getting resource', () => { ['constructor']: typeof MockModel; public static readonly TYPE_NAME: string = TYPE_NAME; } - class Resource extends BaseResource {} + class Resource extends BaseResource {} + + class MockTypeConfigurationModel extends BaseModel { + ['constructor']: typeof MockTypeConfigurationModel; + public static readonly TYPE_NAME: string = TYPE_NAME; + } beforeAll(() => { jest.spyOn(WorkerPoolAwsSdk.prototype, 'runTask').mockRejectedValue( @@ -82,6 +88,9 @@ describe('when getting resource', () => { previousResourceProperties: { state: 'state2' }, stackTags: { tag1: 'abc' }, previousStackTags: { tag1: 'def' }, + typeConfiguration: { + apiToken: 'fklwqrdmlsn', + }, }, stackId: 'arn:aws:cloudformation:us-east-1:123456789012:stack/sample-stack/e722ae60-fe62-11e8-9a0e-0ae8cc519968', @@ -124,8 +133,16 @@ describe('when getting resource', () => { await workerPool.shutdown(); }); - const getResource = (handlers?: HandlerSignatures): Resource => { - const instance = new Resource(TYPE_NAME, MockModel, workerPool, handlers); + const getResource = ( + handlers?: HandlerSignatures + ): Resource => { + const instance = new Resource( + TYPE_NAME, + MockModel, + MockTypeConfigurationModel, + workerPool, + handlers + ); return instance; }; @@ -137,7 +154,7 @@ describe('when getting resource', () => { }); test('entrypoint missing model class', async () => { - const resource = new Resource(TYPE_NAME, null); + const resource = new Resource(TYPE_NAME, null, null); const event = await resource.entrypoint({}, null); expect(event).toMatchObject({ message: 'Error: Missing Model class to be used to deserialize JSON data.', @@ -148,7 +165,7 @@ describe('when getting resource', () => { test('entrypoint success production-like', async () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, mockHandler); const event = await resource.entrypoint(entrypointPayload, null); expect(spyInitializeRuntime).toBeCalledTimes(1); @@ -161,7 +178,7 @@ describe('when getting resource', () => { }); test('publish exception metric without proxy', async () => { - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, jest.fn()); const mockPublishException = jest.fn(); MetricsPublisherProxy.prototype[ @@ -176,7 +193,7 @@ describe('when getting resource', () => { }); test('entrypoint handler raises', async () => { - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); const mockPublishException = jest.fn(); MetricsPublisherProxy.prototype[ 'publishExceptionMetric' @@ -200,7 +217,7 @@ describe('when getting resource', () => { }); test('entrypoint non mutating action', async () => { - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); entrypointPayload['action'] = 'READ'; const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); resource.addHandler(Action.Create, mockHandler); @@ -226,7 +243,7 @@ describe('when getting resource', () => { const mockPublishMessage = jest.fn().mockResolvedValue({}); LambdaLogPublisher.prototype['publishMessage'] = mockPublishMessage; CloudWatchLogPublisher.prototype['publishMessage'] = mockPublishMessage; - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); entrypointPayload['action'] = 'READ'; const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); resource.addHandler(Action.Read, mockHandler); @@ -255,7 +272,7 @@ describe('when getting resource', () => { entrypointPayload['callbackContext'] = { a: 'b' }; const event = ProgressEvent.success(null, { c: 'd' }); const mockHandler: jest.Mock = jest.fn(() => event); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, mockHandler); const response = await resource.entrypoint(entrypointPayload, null); expect(response).toMatchObject({ @@ -268,6 +285,7 @@ describe('when getting resource', () => { expect.any(SessionProxy), expect.any(BaseResourceHandlerRequest), entrypointPayload['callbackContext'], + expect.any(MockTypeConfigurationModel), expect.any(LoggerProxy) ); }); @@ -277,7 +295,7 @@ describe('when getting resource', () => { const event = ProgressEvent.progress(null, { c: 'd' }); event.callbackDelaySeconds = 5; const mockHandler: jest.Mock = jest.fn(() => event); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, mockHandler); const response = await resource.entrypoint(entrypointPayload, null); expect(spyInitializeRuntime).toBeCalledTimes(1); @@ -292,13 +310,40 @@ describe('when getting resource', () => { expect.any(SessionProxy), expect.any(BaseResourceHandlerRequest), {}, + expect.any(MockTypeConfigurationModel), + expect.any(LoggerProxy) + ); + }); + + test('entrypoint without type configuration', async () => { + entrypointPayload['callbackContext'] = { a: 'b' }; + delete entrypointPayload.requestData.typeConfiguration; + const event = ProgressEvent.progress(null, { c: 'd' }); + event.callbackDelaySeconds = 5; + const mockHandler: jest.Mock = jest.fn(() => event); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + resource.addHandler(Action.Create, mockHandler); + const response = await resource.entrypoint(entrypointPayload, null); + expect(spyInitializeRuntime).toBeCalledTimes(1); + expect(response).toMatchObject({ + message: '', + status: OperationStatus.InProgress, + callbackDelaySeconds: 5, + callbackContext: { c: 'd' }, + }); + expect(mockHandler).toBeCalledTimes(1); + expect(mockHandler).toBeCalledWith( + expect.any(SessionProxy), + expect.any(BaseResourceHandlerRequest), + entrypointPayload.callbackContext, + null, expect.any(LoggerProxy) ); }); test('entrypoint success without caller provider creds', async () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, mockHandler); const expected = { message: '', @@ -320,7 +365,7 @@ describe('when getting resource', () => { test('entrypoint with log stream failure', async () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, mockHandler); const spyPrepareLogStream = jest .spyOn(CloudWatchLogHelper.prototype, 'prepareLogStream') @@ -402,7 +447,7 @@ describe('when getting resource', () => { test('parse request valid request and cast resource request', () => { const spyDeserialize: jest.SpyInstance = jest.spyOn(MockModel, 'deserialize'); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); const [ [callerCredentials, providerCredentials], @@ -456,7 +501,7 @@ describe('when getting resource', () => { 'Not allowed to submit a new task after progress tracker has been closed', }); const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, mockHandler); const event = await resource.entrypoint(entrypointPayload, lambdaContext); expect(spyInitializeRuntime).toBeCalledTimes(1); @@ -472,7 +517,7 @@ describe('when getting resource', () => { test('entrypoint success with two consecutive calls', async () => { // We are emulating the execution context reuse in the lambda function const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, mockHandler); jest.spyOn(S3LogHelper.prototype, 'prepareFolder').mockResolvedValue( null @@ -491,7 +536,10 @@ describe('when getting resource', () => { }); test('add handler', () => { - class ResourceEventHandler extends BaseResource { + class ResourceEventHandler extends BaseResource< + MockModel, + MockTypeConfigurationModel + > { @handlerEvent(Action.Create) public create(): void {} @handlerEvent(Action.Read) @@ -503,8 +551,14 @@ describe('when getting resource', () => { @handlerEvent(Action.List) public list(): void {} } - const handlers = new HandlerSignatures(); - const resource = new ResourceEventHandler(null, null, workerPool, handlers); + const handlers = new HandlerSignatures(); + const resource = new ResourceEventHandler( + null, + null, + null, + workerPool, + handlers + ); expect(resource['handlers'].get(Action.Create)).toBe(resource.create); expect(resource['handlers'].get(Action.Read)).toBe(resource.read); expect(resource['handlers'].get(Action.Update)).toBe(resource.update); @@ -513,7 +567,10 @@ describe('when getting resource', () => { }); test('check resource instance and type name', async () => { - class ResourceEventHandler extends BaseResource { + class ResourceEventHandler extends BaseResource< + MockModel, + MockTypeConfigurationModel + > { @handlerEvent(Action.Create) public async create(): Promise> { const progress = ProgressEvent.builder>() @@ -524,10 +581,11 @@ describe('when getting resource', () => { return progress; } } - const handlers = new HandlerSignatures(); + const handlers = new HandlerSignatures(); const resource = new ResourceEventHandler( TYPE_NAME, MockModel, + MockTypeConfigurationModel, workerPool, handlers ); @@ -552,17 +610,19 @@ describe('when getting resource', () => { test('invoke handler was found', async () => { const event = ProgressEvent.progress(); const mockHandler: jest.Mock = jest.fn(() => event); - const handlers = new HandlerSignatures(); + const handlers = new HandlerSignatures(); handlers.set(Action.Create, mockHandler); const resource = getResource(handlers); const session = new SessionProxy({}); const request = new BaseResourceHandlerRequest(); + const typeConf = new MockTypeConfigurationModel(); const callbackContext = {}; const response = await resource['invokeHandler']( session, request, Action.Create, - callbackContext + callbackContext, + typeConf ); expect(response).toBe(event); expect(mockHandler).toBeCalledTimes(1); @@ -570,15 +630,19 @@ describe('when getting resource', () => { session, request, callbackContext, + typeConf, expect.any(LoggerProxy) ); }); test('invoke handler non mutating must be synchronous', async () => { const promises: any[] = []; - [Action.List, Action.Read].forEach(async (action: Action) => { + for (const action of [Action.List, Action.Read]) { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.progress()); - const handlers = new HandlerSignatures(); + const handlers = new HandlerSignatures< + MockModel, + MockTypeConfigurationModel + >(); handlers.set(action, mockHandler); const resource = getResource(handlers); const callbackContext = {}; @@ -593,7 +657,7 @@ describe('when getting resource', () => { } ) ); - }); + } expect.assertions(promises.length); await Promise.all(promises); }); @@ -601,7 +665,7 @@ describe('when getting resource', () => { test('invoke handler try object modification', async () => { const event = ProgressEvent.progress(); const mockHandler: jest.Mock = jest.fn(() => event); - const handlers = new HandlerSignatures(); + const handlers = new HandlerSignatures(); handlers.set(Action.Create, mockHandler); const resource = getResource(handlers); const callbackContext = { @@ -651,7 +715,7 @@ describe('when getting resource', () => { test('parse test request with object literal callback context', () => { const callbackContext = { a: 'b' }; testEntrypointPayload['callbackContext'] = callbackContext; - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload ); @@ -663,7 +727,7 @@ describe('when getting resource', () => { test('parse test request with map callback context', () => { const callbackContext = { a: 'b' }; testEntrypointPayload['callbackContext'] = callbackContext; - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload ); @@ -673,7 +737,7 @@ describe('when getting resource', () => { }); test('parse test request valid request', () => { - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); resource.addHandler(Action.Create, jest.fn()); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload @@ -710,7 +774,7 @@ describe('when getting resource', () => { }); test('test entrypoint missing model class', async () => { - const resource = new Resource(TYPE_NAME, null, workerPool); + const resource = new Resource(TYPE_NAME, null, null, workerPool); const event = await resource.testEntrypoint({}, null); expect(event).toMatchObject({ message: 'Error: Missing Model class to be used to deserialize JSON data.', @@ -721,7 +785,7 @@ describe('when getting resource', () => { test('test entrypoint success', async () => { const spyDeserialize: jest.SpyInstance = jest.spyOn(MockModel, 'deserialize'); - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); const progressEvent = ProgressEvent.progress(); const mockHandler: jest.Mock = jest.fn(() => progressEvent); From 13040bd173cf68dc03ecb144ce80d31e1a818268 Mon Sep 17 00:00:00 2001 From: Duncan Grant Date: Thu, 30 Jun 2022 22:35:43 +0100 Subject: [PATCH 02/11] Add instructions for testing changes to the typescript library from a resource. --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index d231bd5..ebc2a9b 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,19 @@ pip3 install \ That ensures neither is accidentally installed from PyPI. +For changes to the typescript library "@amazon-web-services-cloudformation/cloudformation-cli-typescript-lib" pack up the compiled javascript: + +```shell +npm run build +npm pack +``` + +You can then install this in a cfn resource project using: + +```shell +npm install ../path/to/cloudformation-cli-typescript-plugin/amazon-web-services-cloudformation-cloudformation-cli-typescript-lib-1.0.1.tgz +``` + Linting and running unit tests is done via [pre-commit](https://pre-commit.com/), and so is performed automatically on commit after being installed (`pre-commit install`). The continuous integration also runs these checks. Manual options are available so you don't have to commit: ```shell From 322e23ec0ed57c0ce8ea2432b479efdaa8c587d1 Mon Sep 17 00:00:00 2001 From: Duncan Grant Date: Fri, 1 Jul 2022 11:42:55 +0100 Subject: [PATCH 03/11] Update call signatures to add type configuration --- python/rpdk/typescript/templates/handlers.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/rpdk/typescript/templates/handlers.ts b/python/rpdk/typescript/templates/handlers.ts index 3cb0d9b..0bde884 100644 --- a/python/rpdk/typescript/templates/handlers.ts +++ b/python/rpdk/typescript/templates/handlers.ts @@ -32,6 +32,7 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + typeConfiguration: TypeConfigurationModel, logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); @@ -70,6 +71,7 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + typeConfiguration: TypeConfigurationModel, logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); @@ -95,6 +97,7 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + typeConfiguration: TypeConfigurationModel, logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); @@ -119,6 +122,7 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + typeConfiguration: TypeConfigurationModel, logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); @@ -142,6 +146,7 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + typeConfiguration: TypeConfigurationModel, logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); From a302fd99dc067f0f557119a7e351e60add8adf11 Mon Sep 17 00:00:00 2001 From: Thomas Bouron Date: Mon, 11 Jul 2022 15:16:02 +0200 Subject: [PATCH 04/11] Add JSDoc for the type configuration argument --- python/rpdk/typescript/templates/handlers.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python/rpdk/typescript/templates/handlers.ts b/python/rpdk/typescript/templates/handlers.ts index 0bde884..a7009b4 100644 --- a/python/rpdk/typescript/templates/handlers.ts +++ b/python/rpdk/typescript/templates/handlers.ts @@ -25,6 +25,8 @@ class Resource extends BaseResource { * @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 + * @param typeConfiguration Configuration data for this resource type, in the given account + * and region * @param logger Logger to proxy requests to default publishers */ @handlerEvent(Action.Create) @@ -64,6 +66,8 @@ class Resource extends BaseResource { * @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 + * @param typeConfiguration Configuration data for this resource type, in the given account + * and region * @param logger Logger to proxy requests to default publishers */ @handlerEvent(Action.Update) @@ -90,6 +94,8 @@ class Resource extends BaseResource { * @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 + * @param typeConfiguration Configuration data for this resource type, in the given account + * and region * @param logger Logger to proxy requests to default publishers */ @handlerEvent(Action.Delete) @@ -115,6 +121,8 @@ class Resource extends BaseResource { * @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 + * @param typeConfiguration Configuration data for this resource type, in the given account + * and region * @param logger Logger to proxy requests to default publishers */ @handlerEvent(Action.Read) @@ -139,6 +147,8 @@ class Resource extends BaseResource { * @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 + * @param typeConfiguration Configuration data for this resource type, in the given account + * and region * @param logger Logger to proxy requests to default publishers */ @handlerEvent(Action.List) From de44f36bcb3bcec3bcbd882e366ce2174307c67f Mon Sep 17 00:00:00 2001 From: Thomas Bouron Date: Wed, 20 Jul 2022 15:41:27 +0200 Subject: [PATCH 05/11] Rename `TC` generic to `TypeConfiguration` --- src/resource.ts | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/resource.ts b/src/resource.ts index e692a79..a85adc6 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -45,14 +45,17 @@ const MUTATING_ACTIONS: [Action, Action, Action] = [ Action.Delete, ]; -export type HandlerSignature = Callable< - [Optional, any, Dict, TC, LoggerProxy], +export type HandlerSignature< + T extends BaseModel, + TypeConfiguration extends BaseModel +> = Callable< + [Optional, any, Dict, TypeConfiguration, LoggerProxy], Promise> >; -export class HandlerSignatures extends Map< - Action, - HandlerSignature -> {} +export class HandlerSignatures< + T extends BaseModel, + TypeConfiguration extends BaseModel +> extends Map> {} class HandlerEvents extends Map {} /** @@ -95,7 +98,7 @@ function ensureSerialize(toResponse = false): MethodDecorat export abstract class BaseResource< T extends BaseModel = BaseModel, - TC extends BaseModel = BaseModel + TypeConfiguration extends BaseModel = BaseModel > { protected loggerProxy: LoggerProxy; protected metricsPublisherProxy: MetricsPublisherProxy; @@ -119,14 +122,14 @@ export abstract class BaseResource< constructor( public readonly typeName: string, public readonly modelTypeReference: Constructor, - public readonly typeConfigurationTypeReference: Constructor & { + public readonly typeConfigurationTypeReference: Constructor & { deserialize: Function; }, protected readonly workerPool?: AwsTaskWorkerPool, - private handlers?: HandlerSignatures + private handlers?: HandlerSignatures ) { this.typeName = typeName || ''; - this.handlers = handlers || new HandlerSignatures(); + this.handlers = handlers || new HandlerSignatures(); this.lambdaLogger = console; this.platformLoggerProxy = new LoggerProxy(); @@ -305,8 +308,8 @@ export abstract class BaseResource< public addHandler = ( action: Action, - f: HandlerSignature - ): HandlerSignature => { + f: HandlerSignature + ): HandlerSignature => { this.handlers.set(action, f); return f; }; @@ -316,13 +319,15 @@ export abstract class BaseResource< request: BaseResourceHandlerRequest, action: Action, callbackContext: Dict, - typeConfiguration?: TC + typeConfiguration?: TypeConfiguration ): Promise> => { const actionName = action == null ? '' : action.toString(); if (!this.handlers.has(action)) { throw new Error(`Unknown action ${actionName}`); } - const handleRequest: HandlerSignature = this.handlers.get(action); + const handleRequest: HandlerSignature = this.handlers.get( + action + ); // We will make the callback context and resource states readonly // to avoid modification at a later time deepFreeze(callbackContext); @@ -486,7 +491,9 @@ export abstract class BaseResource< } }; - private castTypeConfigurationRequest = (request: HandlerRequest): TC => { + private castTypeConfigurationRequest = ( + request: HandlerRequest + ): TypeConfiguration => { try { return this.typeConfigurationTypeReference.deserialize( request.requestData.typeConfiguration From 5f33a63d5870d29599c0834fefe77dcebe6a9f26 Mon Sep 17 00:00:00 2001 From: Thomas Bouron Date: Wed, 20 Jul 2022 16:24:49 +0200 Subject: [PATCH 06/11] Fix failing unit test (test was expecting a slightly different exception message) --- tests/lib/resource.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/lib/resource.test.ts b/tests/lib/resource.test.ts index 607fadc..f1481af 100644 --- a/tests/lib/resource.test.ts +++ b/tests/lib/resource.test.ts @@ -442,7 +442,9 @@ describe('when getting resource', () => { resource['castResourceRequest'](request); }; expect(castResourceRequest).toThrow(exceptions.InvalidRequest); - expect(castResourceRequest).toThrow('TypeError: Cannot read property'); + expect(castResourceRequest).toThrow( + "TypeError: Cannot read properties of null (reading 'resourceProperties')" + ); }); test('parse request valid request and cast resource request', () => { From 76aa7bea28d3a60bddc42afdb1ba4f48225fb8d3 Mon Sep 17 00:00:00 2001 From: Duncan Grant Date: Tue, 26 Jul 2022 08:42:20 +0100 Subject: [PATCH 07/11] Change expected error message format --- tests/lib/resource.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/resource.test.ts b/tests/lib/resource.test.ts index f1481af..7239504 100644 --- a/tests/lib/resource.test.ts +++ b/tests/lib/resource.test.ts @@ -443,7 +443,7 @@ describe('when getting resource', () => { }; expect(castResourceRequest).toThrow(exceptions.InvalidRequest); expect(castResourceRequest).toThrow( - "TypeError: Cannot read properties of null (reading 'resourceProperties')" + "TypeError: Cannot read property 'resourceProperties' of null (TypeError)" ); }); From b503e7a080f94b05e0a5e0a4e2264235ea3a2e6e Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Tue, 26 Jul 2022 11:10:25 +0100 Subject: [PATCH 08/11] ignore intellij files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 4612ce0..227448a 100644 --- a/.gitignore +++ b/.gitignore @@ -139,3 +139,5 @@ tests/**/*.d.ts # Node.js node_modules/ coverage/ + +*.iml From 9938c3ee17068278d069ab1f2ed83d48a7d65870 Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Tue, 26 Jul 2022 11:39:34 +0100 Subject: [PATCH 09/11] change signatures to make type configuration optional this makes (1) new version generated code backwards compatible with old version runtime framework (it just won't get type configuration info), and (2) new version runtime framework backwards compatible with old version generated code (it will ignore type configuration info) resulting signature is not especially clean but backwards compatibility is worth it. --- python/rpdk/typescript/templates/handlers.ts | 13 +- src/resource.ts | 22 ++- tests/lib/resource.test.ts | 171 +++++++++++++++---- 3 files changed, 160 insertions(+), 46 deletions(-) diff --git a/python/rpdk/typescript/templates/handlers.ts b/python/rpdk/typescript/templates/handlers.ts index a7009b4..27f0139 100644 --- a/python/rpdk/typescript/templates/handlers.ts +++ b/python/rpdk/typescript/templates/handlers.ts @@ -34,8 +34,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + logger: LoggerProxy, typeConfiguration: TypeConfigurationModel, - logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); const progress = ProgressEvent.progress>(model); @@ -75,8 +75,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + logger: LoggerProxy, typeConfiguration: TypeConfigurationModel, - logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); const progress = ProgressEvent.progress>(model); @@ -103,8 +103,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + logger: LoggerProxy, typeConfiguration: TypeConfigurationModel, - logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); const progress = ProgressEvent.progress>(); @@ -130,8 +130,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + logger: LoggerProxy, typeConfiguration: TypeConfigurationModel, - logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); // TODO: put code here @@ -156,8 +156,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, + logger: LoggerProxy, typeConfiguration: TypeConfigurationModel, - logger: LoggerProxy ): Promise> { const model = new ResourceModel(request.desiredResourceState); // TODO: put code here @@ -169,7 +169,8 @@ class Resource extends BaseResource { } } -export const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel, TypeConfigurationModel); +// @ts-ignore // if running against v1.0.1 or earlier of plugin the 5th argument is not known but best to ignored (runtime code may warn) +export const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel, null, null, TypeConfigurationModel)!; // Entrypoint for production usage after registered in CloudFormation export const entrypoint = resource.entrypoint; diff --git a/src/resource.ts b/src/resource.ts index a85adc6..10fc96f 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -49,7 +49,7 @@ export type HandlerSignature< T extends BaseModel, TypeConfiguration extends BaseModel > = Callable< - [Optional, any, Dict, TypeConfiguration, LoggerProxy], + [Optional, any, Dict, LoggerProxy, TypeConfiguration], Promise> >; export class HandlerSignatures< @@ -122,11 +122,11 @@ export abstract class BaseResource< constructor( public readonly typeName: string, public readonly modelTypeReference: Constructor, - public readonly typeConfigurationTypeReference: Constructor & { - deserialize: Function; - }, protected readonly workerPool?: AwsTaskWorkerPool, - private handlers?: HandlerSignatures + private handlers?: HandlerSignatures, + public readonly typeConfigurationTypeReference?: Constructor & { + deserialize: Function; + } ) { this.typeName = typeName || ''; this.handlers = handlers || new HandlerSignatures(); @@ -337,8 +337,8 @@ export abstract class BaseResource< session, request, callbackContext, - typeConfiguration, - this.loggerProxy || this.platformLoggerProxy + this.loggerProxy || this.platformLoggerProxy, + typeConfiguration ); this.log(`[${action}] handler invoked`); if (handlerResponse != null) { @@ -495,6 +495,14 @@ export abstract class BaseResource< request: HandlerRequest ): TypeConfiguration => { try { + if (!this.typeConfigurationTypeReference) { + if (request.requestData.typeConfiguration) { + throw new InternalFailure( + 'Type configuration supplied but running with legacy version of code which does not support type configuration.' + ); + } + return null; + } return this.typeConfigurationTypeReference.deserialize( request.requestData.typeConfiguration ); diff --git a/tests/lib/resource.test.ts b/tests/lib/resource.test.ts index 7239504..a7784a5 100644 --- a/tests/lib/resource.test.ts +++ b/tests/lib/resource.test.ts @@ -139,9 +139,9 @@ describe('when getting resource', () => { const instance = new Resource( TYPE_NAME, MockModel, - MockTypeConfigurationModel, workerPool, - handlers + handlers, + MockTypeConfigurationModel ); return instance; }; @@ -165,7 +165,13 @@ describe('when getting resource', () => { test('entrypoint success production-like', async () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const event = await resource.entrypoint(entrypointPayload, null); expect(spyInitializeRuntime).toBeCalledTimes(1); @@ -178,7 +184,13 @@ describe('when getting resource', () => { }); test('publish exception metric without proxy', async () => { - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, jest.fn()); const mockPublishException = jest.fn(); MetricsPublisherProxy.prototype[ @@ -193,7 +205,13 @@ describe('when getting resource', () => { }); test('entrypoint handler raises', async () => { - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const mockPublishException = jest.fn(); MetricsPublisherProxy.prototype[ 'publishExceptionMetric' @@ -217,7 +235,13 @@ describe('when getting resource', () => { }); test('entrypoint non mutating action', async () => { - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); entrypointPayload['action'] = 'READ'; const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); resource.addHandler(Action.Create, mockHandler); @@ -243,7 +267,13 @@ describe('when getting resource', () => { const mockPublishMessage = jest.fn().mockResolvedValue({}); LambdaLogPublisher.prototype['publishMessage'] = mockPublishMessage; CloudWatchLogPublisher.prototype['publishMessage'] = mockPublishMessage; - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); entrypointPayload['action'] = 'READ'; const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); resource.addHandler(Action.Read, mockHandler); @@ -272,7 +302,13 @@ describe('when getting resource', () => { entrypointPayload['callbackContext'] = { a: 'b' }; const event = ProgressEvent.success(null, { c: 'd' }); const mockHandler: jest.Mock = jest.fn(() => event); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const response = await resource.entrypoint(entrypointPayload, null); expect(response).toMatchObject({ @@ -285,8 +321,8 @@ describe('when getting resource', () => { expect.any(SessionProxy), expect.any(BaseResourceHandlerRequest), entrypointPayload['callbackContext'], - expect.any(MockTypeConfigurationModel), - expect.any(LoggerProxy) + expect.any(LoggerProxy), + expect.any(MockTypeConfigurationModel) ); }); @@ -295,7 +331,13 @@ describe('when getting resource', () => { const event = ProgressEvent.progress(null, { c: 'd' }); event.callbackDelaySeconds = 5; const mockHandler: jest.Mock = jest.fn(() => event); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const response = await resource.entrypoint(entrypointPayload, null); expect(spyInitializeRuntime).toBeCalledTimes(1); @@ -310,8 +352,8 @@ describe('when getting resource', () => { expect.any(SessionProxy), expect.any(BaseResourceHandlerRequest), {}, - expect.any(MockTypeConfigurationModel), - expect.any(LoggerProxy) + expect.any(LoggerProxy), + expect.any(MockTypeConfigurationModel) ); }); @@ -321,7 +363,13 @@ describe('when getting resource', () => { const event = ProgressEvent.progress(null, { c: 'd' }); event.callbackDelaySeconds = 5; const mockHandler: jest.Mock = jest.fn(() => event); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const response = await resource.entrypoint(entrypointPayload, null); expect(spyInitializeRuntime).toBeCalledTimes(1); @@ -336,14 +384,20 @@ describe('when getting resource', () => { expect.any(SessionProxy), expect.any(BaseResourceHandlerRequest), entrypointPayload.callbackContext, - null, - expect.any(LoggerProxy) + expect.any(LoggerProxy), + null ); }); test('entrypoint success without caller provider creds', async () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const expected = { message: '', @@ -365,7 +419,13 @@ describe('when getting resource', () => { test('entrypoint with log stream failure', async () => { const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const spyPrepareLogStream = jest .spyOn(CloudWatchLogHelper.prototype, 'prepareLogStream') @@ -443,13 +503,22 @@ describe('when getting resource', () => { }; expect(castResourceRequest).toThrow(exceptions.InvalidRequest); expect(castResourceRequest).toThrow( - "TypeError: Cannot read property 'resourceProperties' of null (TypeError)" + // previously tested for (1), but now seeing (2); error message probably depends on version of JS/TS + // (1) "TypeError: Cannot read property 'resourceProperties' of null (TypeError)" + // (2) "TypeError: Cannot read properties of null (reading 'resourceProperties') (TypeError)" + /TypeError: Cannot read propert.*'resourceProperties'.*/ ); }); test('parse request valid request and cast resource request', () => { const spyDeserialize: jest.SpyInstance = jest.spyOn(MockModel, 'deserialize'); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const [ [callerCredentials, providerCredentials], @@ -503,7 +572,13 @@ describe('when getting resource', () => { 'Not allowed to submit a new task after progress tracker has been closed', }); const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const event = await resource.entrypoint(entrypointPayload, lambdaContext); expect(spyInitializeRuntime).toBeCalledTimes(1); @@ -519,7 +594,13 @@ describe('when getting resource', () => { test('entrypoint success with two consecutive calls', async () => { // We are emulating the execution context reuse in the lambda function const mockHandler: jest.Mock = jest.fn(() => ProgressEvent.success()); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); jest.spyOn(S3LogHelper.prototype, 'prepareFolder').mockResolvedValue( null @@ -555,11 +636,11 @@ describe('when getting resource', () => { } const handlers = new HandlerSignatures(); const resource = new ResourceEventHandler( - null, null, null, workerPool, - handlers + handlers, + null ); expect(resource['handlers'].get(Action.Create)).toBe(resource.create); expect(resource['handlers'].get(Action.Read)).toBe(resource.read); @@ -587,9 +668,9 @@ describe('when getting resource', () => { const resource = new ResourceEventHandler( TYPE_NAME, MockModel, - MockTypeConfigurationModel, workerPool, - handlers + handlers, + MockTypeConfigurationModel ); const event = await resource.testEntrypoint(testEntrypointPayload, null); expect(event.status).toBe(OperationStatus.Success); @@ -632,8 +713,8 @@ describe('when getting resource', () => { session, request, callbackContext, - typeConf, - expect.any(LoggerProxy) + expect.any(LoggerProxy), + typeConf ); }); @@ -717,7 +798,13 @@ describe('when getting resource', () => { test('parse test request with object literal callback context', () => { const callbackContext = { a: 'b' }; testEntrypointPayload['callbackContext'] = callbackContext; - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload ); @@ -729,7 +816,13 @@ describe('when getting resource', () => { test('parse test request with map callback context', () => { const callbackContext = { a: 'b' }; testEntrypointPayload['callbackContext'] = callbackContext; - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload ); @@ -739,7 +832,13 @@ describe('when getting resource', () => { }); test('parse test request valid request', () => { - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, jest.fn()); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload @@ -776,7 +875,7 @@ describe('when getting resource', () => { }); test('test entrypoint missing model class', async () => { - const resource = new Resource(TYPE_NAME, null, null, workerPool); + const resource = new Resource(TYPE_NAME, null, workerPool, null); const event = await resource.testEntrypoint({}, null); expect(event).toMatchObject({ message: 'Error: Missing Model class to be used to deserialize JSON data.', @@ -787,7 +886,13 @@ describe('when getting resource', () => { test('test entrypoint success', async () => { const spyDeserialize: jest.SpyInstance = jest.spyOn(MockModel, 'deserialize'); - const resource = new Resource(TYPE_NAME, MockModel, MockTypeConfigurationModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const progressEvent = ProgressEvent.progress(); const mockHandler: jest.Mock = jest.fn(() => progressEvent); From 4564d3a3d0fe07f94d06dc4c69765a3a87eacc5e Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Tue, 26 Jul 2022 17:18:58 +0100 Subject: [PATCH 10/11] more versatile JS error test --- tests/lib/resource.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/lib/resource.test.ts b/tests/lib/resource.test.ts index a7784a5..3fbae82 100644 --- a/tests/lib/resource.test.ts +++ b/tests/lib/resource.test.ts @@ -503,10 +503,11 @@ describe('when getting resource', () => { }; expect(castResourceRequest).toThrow(exceptions.InvalidRequest); expect(castResourceRequest).toThrow( - // previously tested for (1), but now seeing (2); error message probably depends on version of JS/TS + // previously tested for (0) and (1), but now seeing (2); error message probably depends on version of JS/TS + // (0) "TypeError: Cannot read property" // (1) "TypeError: Cannot read property 'resourceProperties' of null (TypeError)" // (2) "TypeError: Cannot read properties of null (reading 'resourceProperties') (TypeError)" - /TypeError: Cannot read propert.*'resourceProperties'.*/ + /TypeError: Cannot read propert(y|.*'resourceProperties'.*)/ ); }); From 7098a110702f34654be0cdcbe996f3d27bae59ce Mon Sep 17 00:00:00 2001 From: Alex Heneveld Date: Tue, 26 Jul 2022 17:19:29 +0100 Subject: [PATCH 11/11] fix missing python lint rule --- python/rpdk/typescript/codegen.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/rpdk/typescript/codegen.py b/python/rpdk/typescript/codegen.py index 59e29c6..f6a9df7 100644 --- a/python/rpdk/typescript/codegen.py +++ b/python/rpdk/typescript/codegen.py @@ -173,6 +173,7 @@ def generate(self, project): path = self.package_root / "models.ts" LOG.debug("Writing file: %s", path) template = self.env.get_template("models.ts") + contents = template.render( lib_name=SUPPORT_LIB_NAME, type_name=project.type_name, @@ -186,6 +187,8 @@ def generate(self, project): LOG.debug("Generate complete") def _pre_package(self, build_path): + # Caller should own/delete this, not us. + # pylint: disable=consider-using-with f = TemporaryFile("w+b") # pylint: disable=unexpected-keyword-arg