diff --git a/packages/aws-cdk-lib/aws-appsync/lib/api-base.ts b/packages/aws-cdk-lib/aws-appsync/lib/api-base.ts index 67b0ef3dca37f..44c531f5ed385 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/api-base.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/api-base.ts @@ -1,9 +1,10 @@ import { IResource, Resource } from '../../core'; +import { IApiRef, ApiReference } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * Interface for an API */ -export interface IApi extends IResource { +export interface IApi extends IResource, IApiRef { /** * The unique identifier for the AWS AppSync Api generated by the service. @@ -33,4 +34,10 @@ export abstract class ApiBase extends Resource implements IApi { * The ARN of the AWS AppSync Api. */ public abstract readonly apiArn: string; + + public get apiRef(): ApiReference { + return { + apiArn: this.apiArn, + }; + } } diff --git a/packages/aws-cdk-lib/aws-appsync/lib/appsync-function.ts b/packages/aws-cdk-lib/aws-appsync/lib/appsync-function.ts index 62389150de30e..21cd81ffbaf30 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/appsync-function.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/appsync-function.ts @@ -8,6 +8,7 @@ import { FunctionRuntime } from './runtime'; import { Resource, IResource, Lazy, Fn, ValidationError } from '../../core'; import { addConstructMetadata } from '../../core/lib/metadata-resource'; import { propertyInjectable } from '../../core/lib/prop-injectable'; +import { IFunctionConfigurationRef, FunctionConfigurationReference } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * the base properties for AppSync Functions @@ -91,7 +92,7 @@ export interface AppsyncFunctionAttributes { /** * Interface for AppSync Functions */ -export interface IAppsyncFunction extends IResource { +export interface IAppsyncFunction extends IResource, IFunctionConfigurationRef { /** * the name of this AppSync Function * @@ -130,6 +131,11 @@ export class AppsyncFunction extends Resource implements IAppsyncFunction { constructor(s: Construct, i: string) { super(s, i); } + public get functionConfigurationRef(): FunctionConfigurationReference { + return { + functionArn: this.functionArn, + }; + } } return new Import(scope, id); } @@ -201,4 +207,10 @@ export class AppsyncFunction extends Resource implements IAppsyncFunction { this.function.addDependency(this.dataSource.ds); props.api.addSchemaDependency(this.function); } + + public get functionConfigurationRef(): FunctionConfigurationReference { + return { + functionArn: this.functionArn, + }; + } } diff --git a/packages/aws-cdk-lib/aws-appsync/lib/channel-namespace.ts b/packages/aws-cdk-lib/aws-appsync/lib/channel-namespace.ts index 3d03ed5655e0b..8a7f4916d2d94 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/channel-namespace.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/channel-namespace.ts @@ -13,11 +13,12 @@ import { IGrantable } from '../../aws-iam'; import { IResource, Resource, Token, ValidationError } from '../../core'; import { addConstructMetadata, MethodMetadata } from '../../core/lib/metadata-resource'; import { propertyInjectable } from '../../core/lib/prop-injectable'; +import { IChannelNamespaceRef, ChannelNamespaceReference } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * An AppSync channel namespace */ -export interface IChannelNamespace extends IResource { +export interface IChannelNamespace extends IResource, IChannelNamespaceRef { /** * The ARN of the AppSync channel namespace * @@ -187,6 +188,11 @@ export class ChannelNamespace extends Resource implements IChannelNamespace { public static fromChannelNamespaceArn(scope: Construct, id: string, channelNamespaceArn: string): IChannelNamespace { class Import extends Resource implements IChannelNamespace { public readonly channelNamespaceArn = channelNamespaceArn; + public get channelNamespaceRef(): ChannelNamespaceReference { + return { + channelNamespaceArn: this.channelNamespaceArn, + }; + } } return new Import(scope, id); } @@ -359,4 +365,10 @@ export class ChannelNamespace extends Resource implements IChannelNamespace { throw new ValidationError('LambdaInvokeType is only supported for Direct handler behavior type', this); } } + + public get channelNamespaceRef(): ChannelNamespaceReference { + return { + channelNamespaceArn: this.channelNamespaceArn, + }; + } } diff --git a/packages/aws-cdk-lib/aws-appsync/lib/data-source-common.ts b/packages/aws-cdk-lib/aws-appsync/lib/data-source-common.ts index 1858089b7cf2e..891bf6c049d1e 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/data-source-common.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/data-source-common.ts @@ -16,6 +16,8 @@ import { IDomain } from '../../aws-opensearchservice'; import { IDatabaseCluster, IServerlessCluster } from '../../aws-rds'; import { ISecret } from '../../aws-secretsmanager'; import { IResolvable, Token, Lazy, Stack } from '../../core'; +import { extractApiIdFromApiRef, toIApi } from './private/ref-utils'; +import { IApiRef } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * Valid data source types for AppSync @@ -83,7 +85,7 @@ export interface AppSyncBaseDataSourceProps { /** * The API to attach this data source to */ - readonly api: IApi; + readonly api: IApiRef; /** * The name of the data source. The only allowed pattern is: {[_A-Za-z][_0-9A-Za-z]*}. * Any invalid characters will be automatically removed. @@ -171,7 +173,7 @@ export abstract class AppSyncBaseDataSource extends Construct { */ public readonly resource: CfnDataSource; - protected api: IApi; + private _api: IApiRef; protected serviceRole?: IRole; constructor(scope: Construct, id: string, props: AppSyncBackedDataSourceProps, extended: AppSyncExtendedDataSourceProps) { @@ -182,15 +184,30 @@ export abstract class AppSyncBaseDataSource extends Construct { // Replace unsupported characters from DataSource name. The only allowed pattern is: {[_A-Za-z][_0-9A-Za-z]*} const name = (props.name ?? id); const supportedName = Token.isUnresolved(name) ? name : name.replace(/[\W]+/g, ''); + const apiId = extractApiIdFromApiRef(props.api); this.resource = new CfnDataSource(this, 'Resource', { - apiId: props.api.apiId, + apiId: apiId, name: supportedName, description: props.description, serviceRoleArn: this.serviceRole?.roleArn, ...extended, }); this.name = supportedName; - this.api = props.api; + this._api = props.api; + } + + /** + * The API this data source is attached to + */ + protected get api(): IApi { + return toIApi(this._api); + } + + /** + * Set the API this data source is attached to + */ + protected set api(api: IApi) { + this._api = api; } } diff --git a/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts b/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts index fd92f4f9a9542..62583febb0ce0 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/data-source.ts @@ -2,6 +2,7 @@ import { Construct } from 'constructs'; import { BaseAppsyncFunctionProps, AppsyncFunction } from './appsync-function'; import { CfnDataSource } from './appsync.generated'; import { IGraphqlApi } from './graphqlapi-base'; +import { extractApiIdFromGraphQLApiRef, toIGraphqlApi } from './private/ref-utils'; import { BaseResolverProps, Resolver } from './resolver'; import { ITable } from '../../aws-dynamodb'; import { IDomain as IElasticsearchDomain } from '../../aws-elasticsearch'; @@ -13,6 +14,7 @@ import { IServerlessCluster, IDatabaseCluster } from '../../aws-rds'; import { ISecret } from '../../aws-secretsmanager'; import { IResolvable, Lazy, Stack, Token } from '../../core'; import { propertyInjectable } from '../../core/lib/prop-injectable'; +import { IGraphQLApiRef } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * Base properties for an AppSync datasource @@ -21,7 +23,7 @@ export interface BaseDataSourceProps { /** * The API to attach this data source to */ - readonly api: IGraphqlApi; + readonly api: IGraphQLApiRef; /** * The name of the data source * @@ -116,7 +118,7 @@ export abstract class BaseDataSource extends Construct { */ public readonly ds: CfnDataSource; - protected api: IGraphqlApi; + private _api: IGraphQLApiRef; protected serviceRole?: IRole; constructor(scope: Construct, id: string, props: BackedDataSourceProps, extended: ExtendedDataSourceProps) { @@ -128,15 +130,30 @@ export abstract class BaseDataSource extends Construct { // Replace unsupported characters from DataSource name. The only allowed pattern is: {[_A-Za-z][_0-9A-Za-z]*} const name = (props.name ?? id); const supportedName = Token.isUnresolved(name) ? name : name.replace(/[\W]+/g, ''); + const apiId = extractApiIdFromGraphQLApiRef(props.api); this.ds = new CfnDataSource(this, 'Resource', { - apiId: props.api.apiId, + apiId: apiId, name: supportedName, description: props.description, serviceRoleArn: this.serviceRole?.roleArn, ...extended, }); this.name = supportedName; - this.api = props.api; + this._api = props.api; + } + + /** + * The API this data source is attached to + */ + protected get api(): IGraphqlApi { + return toIGraphqlApi(this._api); + } + + /** + * Set the API this data source is attached to + */ + protected set api(api: IGraphqlApi) { + this._api = api; } /** diff --git a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts index bee46be04dd73..29531a8d8bb35 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/graphqlapi-base.ts @@ -19,6 +19,7 @@ import { IDomain as IOpenSearchDomain } from '../../aws-opensearchservice'; import { IDatabaseCluster, IServerlessCluster } from '../../aws-rds'; import { ISecret } from '../../aws-secretsmanager'; import { ArnFormat, CfnResource, IResource, Resource, Stack, UnscopedValidationError } from '../../core'; +import { IGraphQLApiRef, GraphQLApiReference } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * Optional configuration for data sources @@ -156,7 +157,7 @@ export enum AuthorizationType { /** * Interface for GraphQL */ -export interface IGraphqlApi extends IResource { +export interface IGraphqlApi extends IResource, IGraphQLApiRef { /** * an unique AWS AppSync GraphQL API identifier @@ -595,4 +596,10 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { public grantSubscription(grantee: IGrantable, ...fields: string[]): Grant { return this.grant(grantee, IamResource.ofType('Subscription', ...fields), 'appsync:GraphQL'); } + + public get graphQlApiRef(): GraphQLApiReference { + return { + graphQlApiArn: this.arn, + }; + } } diff --git a/packages/aws-cdk-lib/aws-appsync/lib/private/ref-utils.ts b/packages/aws-cdk-lib/aws-appsync/lib/private/ref-utils.ts new file mode 100644 index 0000000000000..362d9f47a6625 --- /dev/null +++ b/packages/aws-cdk-lib/aws-appsync/lib/private/ref-utils.ts @@ -0,0 +1,77 @@ +import { Fn, UnscopedValidationError } from '../../../core'; +import { IGraphQLApiRef, IApiRef, IFunctionConfigurationRef } from '../../../interfaces/generated/aws-appsync-interfaces.generated'; +import { IApi } from '../api-base'; +import { IAppsyncFunction } from '../appsync-function'; +import { IGraphqlApi } from '../graphqlapi-base'; + +/** + * Converts an IGraphQLApiRef to IGraphqlApi, validating that it implements the full interface + */ +export function toIGraphqlApi(api: IGraphQLApiRef): IGraphqlApi { + if (!isGraphQlApi(api)) { + throw new UnscopedValidationError(`'api' instance should implement IGraphqlApi, but doesn't: ${api.constructor?.name ?? 'unknown'}`); + } + return api; +} + +function isGraphQlApi(apiRef: IGraphQLApiRef): apiRef is IGraphqlApi { + const api = apiRef as any; + if (typeof api.apiId !== 'string' || typeof api.arn !== 'string' || typeof api.addNoneDataSource !== 'function') { + return false; + } + return true; +} + +function isIApi(apiRef: IApiRef): apiRef is IApi { + const api = apiRef as any; + if (typeof api.apiId !== 'string' || typeof api.apiArn !== 'string' || typeof api.addDynamoDbDataSource !== 'function') { + return false; + } + return true; +} + +/** + * Converts an IApiRef to IApi, validating that it implements the full interface + */ +export function toIApi(api: IApiRef): IApi { + if (!isIApi(api)) { + throw new UnscopedValidationError(`'api' instance should implement IApi, but doesn't: ${api.constructor?.name ?? 'unknown'}`); + } + return api; +} + +export function extractApiIdFromApiRef(apiRef: IApiRef): string { + // Check if this is actually an IApi (which has apiId directly) + if (isIApi(apiRef)) { + return apiRef.apiId; + } + + // Otherwise, extract from the ARN + // ARN format: arn:aws:appsync:region:account:apis/ + return Fn.select(1, Fn.split('/', apiRef.apiRef.apiArn)); +} + +export function extractApiIdFromGraphQLApiRef(apiRef: IGraphQLApiRef): string { + // Check if this is actually an IGraphqlApi (which has apiId directly) + if (isGraphQlApi(apiRef)) { + return apiRef.apiId; + } + // Otherwise, extract from the ARN + // ARN format: arn:aws:appsync:region:account:apis/ + return Fn.select(1, Fn.split('/', apiRef.graphQlApiRef.graphQlApiArn)); +} + +function isIFunctionConfiguration(funcRef: IFunctionConfigurationRef): funcRef is IAppsyncFunction { + const fr = funcRef as unknown as IAppsyncFunction; + return !!fr.functionId; +} + +export function extractFunctionIdFromFunctionRef(funcRef: IFunctionConfigurationRef): string { + // Check if this is actually an IAppsyncFunction (which has functionId directly) + if (isIFunctionConfiguration(funcRef)) { + return funcRef.functionId; + } + // Otherwise, extract from the ARN + // ARN format: arn:aws:appsync:region:account:apis//functions/ + return Fn.select(3, Fn.split('/', funcRef.functionConfigurationRef.functionArn)); +} diff --git a/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts b/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts index cbd7b3c18dcfd..4246108fc12ff 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/resolver.ts @@ -1,5 +1,4 @@ import { Construct } from 'constructs'; -import { IAppsyncFunction } from './appsync-function'; import { CfnResolver } from './appsync.generated'; import { CachingConfig } from './caching-config'; import { BASE_CACHING_KEYS } from './caching-key'; @@ -9,6 +8,8 @@ import { IGraphqlApi } from './graphqlapi-base'; import { MappingTemplate } from './mapping-template'; import { FunctionRuntime } from './runtime'; import { Token, ValidationError } from '../../core'; +import { extractFunctionIdFromFunctionRef } from './private/ref-utils'; +import { IFunctionConfigurationRef } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * Basic properties for an AppSync resolver @@ -28,7 +29,7 @@ export interface BaseResolverProps { * @default - no pipeline resolver configuration * An empty array | undefined sets resolver to be of kind, unit */ - readonly pipelineConfig?: IAppsyncFunction[]; + readonly pipelineConfig?: IFunctionConfigurationRef[]; /** * The request mapping template for this resolver * @@ -105,7 +106,7 @@ export class Resolver extends Construct { super(scope, id); const pipelineConfig = props.pipelineConfig && props.pipelineConfig.length ? - { functions: props.pipelineConfig.map((func) => func.functionId) } + { functions: props.pipelineConfig.map((func) => extractFunctionIdFromFunctionRef(func)) } : undefined; // If runtime is specified, code must also be diff --git a/packages/aws-cdk-lib/aws-appsync/lib/schema.ts b/packages/aws-cdk-lib/aws-appsync/lib/schema.ts index f9281176b3b9c..888fd5efab6b1 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/schema.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/schema.ts @@ -1,5 +1,6 @@ import { readFileSync } from 'fs'; -import { IGraphqlApi } from './graphqlapi-base'; +import { extractApiIdFromGraphQLApiRef } from './private/ref-utils'; +import { IGraphQLApiRef } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * Configuration for bound graphql schema @@ -39,7 +40,7 @@ export interface ISchema { * @param api the api to bind the schema to * @param options configuration for bind behavior */ - bind(api: IGraphqlApi, options?: SchemaBindOptions): ISchemaConfig; + bind(api: IGraphQLApiRef, options?: SchemaBindOptions): ISchemaConfig; } /** @@ -86,9 +87,9 @@ export class SchemaFile implements ISchema { * * @param api The binding GraphQL Api */ - public bind(api: IGraphqlApi, _options?: SchemaBindOptions): ISchemaConfig { + public bind(api: IGraphQLApiRef, _options?: SchemaBindOptions): ISchemaConfig { return { - apiId: api.apiId, + apiId: extractApiIdFromGraphQLApiRef(api), definition: this.definition, }; } diff --git a/packages/aws-cdk-lib/aws-appsync/lib/source-api-association.ts b/packages/aws-cdk-lib/aws-appsync/lib/source-api-association.ts index ee8122fea9598..3844093578dda 100644 --- a/packages/aws-cdk-lib/aws-appsync/lib/source-api-association.ts +++ b/packages/aws-cdk-lib/aws-appsync/lib/source-api-association.ts @@ -5,6 +5,7 @@ import { Effect, IRole, PolicyStatement } from '../../aws-iam'; import { Fn, IResource, Lazy, Resource } from '../../core'; import { addConstructMetadata } from '../../core/lib/metadata-resource'; import { propertyInjectable } from '../../core/lib/prop-injectable'; +import { ISourceApiAssociationRef, SourceApiAssociationReference } from '../../interfaces/generated/aws-appsync-interfaces.generated'; /** * Merge type used to associate the source API @@ -24,7 +25,7 @@ export enum MergeType { /** * Interface for AppSync Source Api Association */ -export interface ISourceApiAssociation extends IResource { +export interface ISourceApiAssociation extends IResource, ISourceApiAssociationRef { /** * The association id. @@ -126,6 +127,11 @@ export class SourceApiAssociation extends Resource implements ISourceApiAssociat constructor(s: Construct, i: string) { super(s, i); } + public get sourceApiAssociationRef(): SourceApiAssociationReference { + return { + associationArn: this.associationArn, + }; + } } return new Import(scope, id); } @@ -197,6 +203,12 @@ export class SourceApiAssociation extends Resource implements ISourceApiAssociat addSourceApiAutoMergePermission(this.association, this.mergedApiExecutionRole); } } + + public get sourceApiAssociationRef(): SourceApiAssociationReference { + return { + associationArn: this.associationArn, + }; + } } /**