packages/aws-cdk-lib/aws-appsync/lib/channel-namespace.ts (173 lines of code) (raw):
import { Construct } from 'constructs';
import { AppSyncEventResource } from './appsync-common';
import { CfnChannelNamespace } from './appsync.generated';
import { AppSyncAuthorizationType } from './auth-config';
import { Code } from './code';
import {
AppSyncBackedDataSource,
AppSyncDataSourceType,
LambdaInvokeType,
} from './data-source-common';
import { IEventApi } from './eventapi';
import { IGrantable } from '../../aws-iam';
import { IResource, Resource, Token, ValidationError } from '../../core';
import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource';
/**
* An AppSync channel namespace
*/
export interface IChannelNamespace extends IResource {
/**
* The ARN of the AppSync channel namespace
*
* @attribute
*/
readonly channelNamespaceArn: string;
}
/**
* Authorization configuration for the Channel Namespace
*/
export interface NamespaceAuthConfig {
/**
* The publish auth modes for this Event Api
* @default - API Key authorization
*/
readonly publishAuthModeTypes?: AppSyncAuthorizationType[];
/**
* The subscribe auth modes for this Event Api
* @default - API Key authorization
*/
readonly subscribeAuthModeTypes?: AppSyncAuthorizationType[];
}
/**
* Enumerated type for the handler behavior for a channel namespace
*/
export enum HandlerBehavior {
/**
* Code handler
*/
CODE = 'CODE',
/**
* Direct integration handler
*/
DIRECT = 'DIRECT',
}
/**
* Handler configuration construct for onPublish and onSubscribe
*/
export interface HandlerConfig {
/**
* If the Event Handler should invoke the data source directly
*
* @default - false
*/
readonly direct?: boolean;
/**
* The Event Handler data source
*
* @default - no data source is used
*/
readonly dataSource?: AppSyncBackedDataSource;
/**
* The Lambda invocation type for direct integrations
*
* @default - LambdaInvokeType.REQUEST_RESPONSE
*/
readonly lambdaInvokeType?: LambdaInvokeType;
}
/**
* the base properties for a channel namespace
*/
export interface BaseChannelNamespaceProps {
/**
* the name of the channel namespace
*
* @default - the construct's id will be used
*/
readonly channelNamespaceName?: string;
/**
* The Event Handler code
*
* @default - no code is used
*/
readonly code?: Code;
/**
* onPublish handler config
*
* @default - no handler config
*/
readonly publishHandlerConfig?: HandlerConfig;
/**
* onSubscribe handler config
*
* @default - no handler config
*/
readonly subscribeHandlerConfig?: HandlerConfig;
/**
* Authorization config for channel namespace
*
* @default - defaults to Event API default auth config
*/
readonly authorizationConfig?: NamespaceAuthConfig;
}
/**
* Additional property for an AppSync channel namespace for an Event API reference
*/
export interface ChannelNamespaceProps extends BaseChannelNamespaceProps {
/**
* The API this channel namespace is associated with
*/
readonly api: IEventApi;
}
/**
* Option configuration for channel namespace
*/
export interface ChannelNamespaceOptions {
/**
* The Channel Namespace name
*
* @default - the construct's id will be used
*/
readonly channelNamespaceName?: string;
/**
* The Event Handler code
*
* @default - no code is used
*/
readonly code?: Code;
/**
* onPublish handler config
*
* @default - no handler config
*/
readonly publishHandlerConfig?: HandlerConfig;
/**
* onSubscribe handler config
*
* @default - no handler config
*/
readonly subscribeHandlerConfig?: HandlerConfig;
/**
* Authorization config for channel namespace
*
* @default - defaults to Event API default auth config
*/
readonly authorizationConfig?: NamespaceAuthConfig;
}
/**
* A Channel Namespace
*/
export class ChannelNamespace extends Resource implements IChannelNamespace {
/**
* Use an existing channel namespace by ARN
*/
public static fromChannelNamespaceArn(scope: Construct, id: string, channelNamespaceArn: string): IChannelNamespace {
class Import extends Resource implements IChannelNamespace {
public readonly channelNamespaceArn = channelNamespaceArn;
}
return new Import(scope, id);
}
/**
* the ARN of the channel namespace
*/
public readonly channelNamespaceArn: string;
private channelNamespace: CfnChannelNamespace;
private api: IEventApi;
constructor(scope: Construct, id: string, props: ChannelNamespaceProps) {
if (props.channelNamespaceName !== undefined && !Token.isUnresolved(props.channelNamespaceName)) {
if (props.channelNamespaceName.length < 1 || props.channelNamespaceName.length > 50) {
throw new ValidationError(`\`channelNamespaceName\` must be between 1 and 50 characters, got: ${props.channelNamespaceName.length} characters.`, scope);
}
}
super(scope, id, {
physicalName: props.channelNamespaceName ?? id,
});
// Enhanced CDK Analytics Telemetry
addConstructMetadata(this, props);
this.validateHandlerConfig(props);
const code = props.code?.bind(this);
let handlerConfig: { [key: string]: any } = {};
if (props.publishHandlerConfig) {
handlerConfig = {
onPublish: {
behavior: props.publishHandlerConfig?.direct ? HandlerBehavior.DIRECT : HandlerBehavior.CODE,
integration: {
dataSourceName: props.publishHandlerConfig?.dataSource?.name || '',
},
},
};
if (handlerConfig.onPublish.behavior === HandlerBehavior.DIRECT) {
handlerConfig.onPublish.integration.lambdaConfig = {
invokeType: props.publishHandlerConfig?.lambdaInvokeType || LambdaInvokeType.REQUEST_RESPONSE,
};
}
}
if (props.subscribeHandlerConfig) {
handlerConfig = {
...handlerConfig,
onSubscribe: {
behavior: props.subscribeHandlerConfig?.direct ? HandlerBehavior.DIRECT : HandlerBehavior.CODE,
integration: {
dataSourceName: props.subscribeHandlerConfig?.dataSource?.name || '',
},
},
};
if (handlerConfig.onSubscribe.behavior === HandlerBehavior.DIRECT) {
handlerConfig.onSubscribe.integration.lambdaConfig = {
invokeType: props.subscribeHandlerConfig?.lambdaInvokeType || LambdaInvokeType.REQUEST_RESPONSE,
};
}
}
this.validateAuthorizationConfig(props.api.authProviderTypes, props.authorizationConfig?.publishAuthModeTypes ?? []);
this.validateAuthorizationConfig(props.api.authProviderTypes, props.authorizationConfig?.subscribeAuthModeTypes ?? []);
this.channelNamespace = new CfnChannelNamespace(this, 'Resource', {
name: this.physicalName,
apiId: props.api.apiId,
codeHandlers: code?.inlineCode,
codeS3Location: code?.s3Location,
handlerConfigs: handlerConfig,
publishAuthModes: props.authorizationConfig?.publishAuthModeTypes?.map((mode) => ({
authType: mode,
})),
subscribeAuthModes: props.authorizationConfig?.subscribeAuthModeTypes?.map((mode) => ({
authType: mode,
})),
});
if (props.publishHandlerConfig?.dataSource) {
this.channelNamespace.addDependency(props.publishHandlerConfig.dataSource.resource);
}
if (props.subscribeHandlerConfig?.dataSource) {
this.channelNamespace.addDependency(props.subscribeHandlerConfig.dataSource.resource);
}
this.channelNamespaceArn = this.channelNamespace.attrChannelNamespaceArn;
this.api = props.api;
}
/**
* Adds an IAM policy statement for EventSubscribe access to this channel namespace to an IAM
* principal's policy.
*
* @param grantee The principal
*/
@MethodMetadata()
public grantSubscribe(grantee: IGrantable) {
return this.api.grant(grantee, AppSyncEventResource.ofChannelNamespace(this.channelNamespace.name), 'appsync:EventSubscribe');
}
/**
* Adds an IAM policy statement for EventPublish access to this channel namespace to an IAM
* principal's policy.
*
* @param grantee The principal
*/
@MethodMetadata()
public grantPublish(grantee: IGrantable) {
return this.api.grant(grantee, AppSyncEventResource.ofChannelNamespace(this.channelNamespace.name), 'appsync:EventPublish');
}
/**
* Adds an IAM policy statement for EventPublish and EventSubscribe access to this channel namespace to an IAM
* principal's policy.
*
* @param grantee The principal
*/
@MethodMetadata()
public grantPublishAndSubscribe(grantee: IGrantable) {
return this.api.grant(grantee, AppSyncEventResource.ofChannelNamespace(this.channelNamespace.name), 'appsync:EventPublish', 'appsync:EventSubscribe');
}
private validateAuthorizationConfig(apiAuthProviders: AppSyncAuthorizationType[], channelAuthModeTypes: AppSyncAuthorizationType[]) {
for (const mode of channelAuthModeTypes) {
if (!apiAuthProviders.find((authProvider) => authProvider === mode)) {
throw new ValidationError(`API is missing authorization configuration for ${mode}`, this);
}
}
}
private validateHandlerConfig(props?: ChannelNamespaceProps) {
// Handle the case when no handler configs are defined for publish or subscribe
if (!props?.publishHandlerConfig && !props?.subscribeHandlerConfig) return undefined;
// Handle the case where behavior is direct but Lambda is not the data source
if (props.publishHandlerConfig?.direct) {
if (!props.publishHandlerConfig.dataSource) {
throw new ValidationError('No data source provided. AWS_LAMBDA data source is required for Direct handler behavior type', this);
}
if (props.publishHandlerConfig.dataSource.resource.type !== AppSyncDataSourceType.LAMBDA) {
throw new ValidationError('Direct integration is only supported for AWS_LAMBDA data sources.', this);
}
}
if (props.subscribeHandlerConfig?.direct) {
if (!props.subscribeHandlerConfig.dataSource) {
throw new ValidationError('No data source provided. AWS_LAMBDA data source is required for Direct handler behavior type', this);
}
if (props.subscribeHandlerConfig.dataSource.resource.type !== AppSyncDataSourceType.LAMBDA) {
throw new ValidationError('Direct integration is only supported for AWS_LAMBDA data sources.', this);
}
}
// Handle the case where behavior is direct for both publish and subscribe, but code handler is provided
if (props.publishHandlerConfig?.direct && props.subscribeHandlerConfig?.direct && props.code) {
throw new ValidationError('Code handlers are not supported when both publish and subscribe use the Direct data source behavior', this);
}
// Handle the case where behavior is code and Lambda invoke type is specified
if (!props.publishHandlerConfig?.direct && props.publishHandlerConfig?.lambdaInvokeType) {
throw new ValidationError('LambdaInvokeType is only supported for Direct handler behavior type', this);
}
// Handle the case where behavior is code and Lambda invoke type is specified
if (!props.subscribeHandlerConfig?.direct && props.subscribeHandlerConfig?.lambdaInvokeType) {
throw new ValidationError('LambdaInvokeType is only supported for Direct handler behavior type', this);
}
}
}