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 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..ebc2a9b 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** @@ -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 diff --git a/python/rpdk/typescript/codegen.py b/python/rpdk/typescript/codegen.py index b23c7ae..f6a9df7 100644 --- a/python/rpdk/typescript/codegen.py +++ b/python/rpdk/typescript/codegen.py @@ -161,13 +161,24 @@ 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") + contents = template.render( 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", []), ) @@ -176,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 diff --git a/python/rpdk/typescript/templates/handlers.ts b/python/rpdk/typescript/templates/handlers.ts index 60300c3..27f0139 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 {} @@ -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) @@ -32,7 +34,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, - logger: LoggerProxy + logger: LoggerProxy, + typeConfiguration: TypeConfigurationModel, ): Promise> { const model = new ResourceModel(request.desiredResourceState); const progress = ProgressEvent.progress>(model); @@ -63,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) @@ -70,7 +75,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, - logger: LoggerProxy + logger: LoggerProxy, + typeConfiguration: TypeConfigurationModel, ): Promise> { const model = new ResourceModel(request.desiredResourceState); const progress = ProgressEvent.progress>(model); @@ -88,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) @@ -95,7 +103,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, - logger: LoggerProxy + logger: LoggerProxy, + typeConfiguration: TypeConfigurationModel, ): Promise> { const model = new ResourceModel(request.desiredResourceState); const progress = ProgressEvent.progress>(); @@ -112,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) @@ -119,7 +130,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, - logger: LoggerProxy + logger: LoggerProxy, + typeConfiguration: TypeConfigurationModel, ): Promise> { const model = new ResourceModel(request.desiredResourceState); // TODO: put code here @@ -135,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) @@ -142,7 +156,8 @@ class Resource extends BaseResource { session: Optional, request: ResourceHandlerRequest, callbackContext: CallbackContext, - logger: LoggerProxy + logger: LoggerProxy, + typeConfiguration: TypeConfigurationModel, ): Promise> { const model = new ResourceModel(request.desiredResourceState); // TODO: put code here @@ -154,7 +169,8 @@ class Resource extends BaseResource { } } -export const resource = new Resource(ResourceModel.TYPE_NAME, ResourceModel); +// @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/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..10fc96f 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,14 +45,17 @@ const MUTATING_ACTIONS: [Action, Action, Action] = [ Action.Delete, ]; -export type HandlerSignature = Callable< - [Optional, any, Dict, LoggerProxy], +export type HandlerSignature< + T extends BaseModel, + TypeConfiguration extends BaseModel +> = Callable< + [Optional, any, Dict, LoggerProxy, TypeConfiguration], Promise> >; -export class HandlerSignatures extends Map< - Action, - HandlerSignature -> {} +export class HandlerSignatures< + T extends BaseModel, + TypeConfiguration extends BaseModel +> extends Map> {} class HandlerEvents extends Map {} /** @@ -88,7 +96,10 @@ function ensureSerialize(toResponse = false): MethodDecorat }; } -export abstract class BaseResource { +export abstract class BaseResource< + T extends BaseModel = BaseModel, + TypeConfiguration extends BaseModel = BaseModel +> { protected loggerProxy: LoggerProxy; protected metricsPublisherProxy: MetricsPublisherProxy; @@ -112,10 +123,13 @@ export abstract class BaseResource { public readonly typeName: string, public readonly modelTypeReference: Constructor, protected readonly workerPool?: AwsTaskWorkerPool, - private handlers?: HandlerSignatures + private handlers?: HandlerSignatures, + public readonly typeConfigurationTypeReference?: Constructor & { + deserialize: Function; + } ) { this.typeName = typeName || ''; - this.handlers = handlers || new HandlerSignatures(); + this.handlers = handlers || new HandlerSignatures(); this.lambdaLogger = console; this.platformLoggerProxy = new LoggerProxy(); @@ -294,8 +308,8 @@ export abstract class BaseResource { public addHandler = ( action: Action, - f: HandlerSignature - ): HandlerSignature => { + f: HandlerSignature + ): HandlerSignature => { this.handlers.set(action, f); return f; }; @@ -304,13 +318,16 @@ export abstract class BaseResource { session: Optional, request: BaseResourceHandlerRequest, action: Action, - callbackContext: Dict + callbackContext: Dict, + 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); @@ -320,7 +337,8 @@ export abstract class BaseResource { session, request, callbackContext, - this.loggerProxy || this.platformLoggerProxy + this.loggerProxy || this.platformLoggerProxy, + typeConfiguration ); this.log(`[${action}] handler invoked`); if (handlerResponse != null) { @@ -473,6 +491,27 @@ export abstract class BaseResource { } }; + private castTypeConfigurationRequest = ( + 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 + ); + } 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 +539,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 +591,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..3fbae82 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, + workerPool, + handlers, + MockTypeConfigurationModel + ); 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,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); + 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); @@ -161,7 +184,13 @@ 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, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, jest.fn()); const mockPublishException = jest.fn(); MetricsPublisherProxy.prototype[ @@ -176,7 +205,13 @@ describe('when getting resource', () => { }); test('entrypoint handler raises', async () => { - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const mockPublishException = jest.fn(); MetricsPublisherProxy.prototype[ 'publishExceptionMetric' @@ -200,7 +235,13 @@ describe('when getting resource', () => { }); test('entrypoint non mutating action', async () => { - const resource = new Resource(TYPE_NAME, MockModel); + 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); @@ -226,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); + 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); @@ -255,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); + 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({ @@ -268,7 +321,8 @@ describe('when getting resource', () => { expect.any(SessionProxy), expect.any(BaseResourceHandlerRequest), entrypointPayload['callbackContext'], - expect.any(LoggerProxy) + expect.any(LoggerProxy), + expect.any(MockTypeConfigurationModel) ); }); @@ -277,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); + 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); @@ -292,13 +352,52 @@ describe('when getting resource', () => { expect.any(SessionProxy), expect.any(BaseResourceHandlerRequest), {}, - expect.any(LoggerProxy) + expect.any(LoggerProxy), + expect.any(MockTypeConfigurationModel) + ); + }); + + 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, + null, + null, + 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, + 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); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const expected = { message: '', @@ -320,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); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); const spyPrepareLogStream = jest .spyOn(CloudWatchLogHelper.prototype, 'prepareLogStream') @@ -397,12 +502,24 @@ describe('when getting resource', () => { resource['castResourceRequest'](request); }; expect(castResourceRequest).toThrow(exceptions.InvalidRequest); - expect(castResourceRequest).toThrow('TypeError: Cannot read property'); + expect(castResourceRequest).toThrow( + // 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(y|.*'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); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const [ [callerCredentials, providerCredentials], @@ -456,7 +573,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); + 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); @@ -472,7 +595,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); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, mockHandler); jest.spyOn(S3LogHelper.prototype, 'prepareFolder').mockResolvedValue( null @@ -491,7 +620,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 +635,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, + workerPool, + handlers, + null + ); 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 +651,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,12 +665,13 @@ describe('when getting resource', () => { return progress; } } - const handlers = new HandlerSignatures(); + const handlers = new HandlerSignatures(); const resource = new ResourceEventHandler( TYPE_NAME, MockModel, workerPool, - handlers + handlers, + MockTypeConfigurationModel ); const event = await resource.testEntrypoint(testEntrypointPayload, null); expect(event.status).toBe(OperationStatus.Success); @@ -552,17 +694,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 +714,19 @@ describe('when getting resource', () => { session, request, callbackContext, - expect.any(LoggerProxy) + expect.any(LoggerProxy), + typeConf ); }); 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 +741,7 @@ describe('when getting resource', () => { } ) ); - }); + } expect.assertions(promises.length); await Promise.all(promises); }); @@ -601,7 +749,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 +799,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); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload ); @@ -663,7 +817,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); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload ); @@ -673,7 +833,13 @@ describe('when getting resource', () => { }); test('parse test request valid request', () => { - const resource = new Resource(TYPE_NAME, MockModel); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); resource.addHandler(Action.Create, jest.fn()); const [request, action, callback] = resource['parseTestRequest']( testEntrypointPayload @@ -710,7 +876,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, workerPool, null); const event = await resource.testEntrypoint({}, null); expect(event).toMatchObject({ message: 'Error: Missing Model class to be used to deserialize JSON data.', @@ -721,7 +887,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); + const resource = new Resource( + TYPE_NAME, + MockModel, + null, + null, + MockTypeConfigurationModel + ); const progressEvent = ProgressEvent.progress(); const mockHandler: jest.Mock = jest.fn(() => progressEvent);