Skip to content
Closed
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,15 @@ export interface S3Location {
/**
* Properties required to prepare the api specification with the given integrations, authorizers, etc
*/
export interface PrepareApiSpecCustomResourceProperties
extends PrepareApiSpecOptions {
export interface PrepareApiSpecCustomResourceProperties {
/**
* The location from which to read the spec to prepare
*/
readonly inputSpecLocation: S3Location;
/**
* The location from which to read the options for the input spec (integrations, cors, authorizers, etc)
*/
readonly inputOptionsLocation: S3Location;
/**
* The location to write the prepared spec. Note that the key is used as a prefix and the output location will
* include a hash.
Expand Down Expand Up @@ -86,14 +89,14 @@ const s3 = new S3Client({
/**
* Prepare the api spec for API Gateway
* @param inputSpecLocation location of the specification to prepare
* @param inputOptionsLocation location of the options for the input spec (integrations, cors, authorizers, etc)
* @param outputSpecLocation location to write the prepared spec to
* @param options integrations, authorizers etc to apply
* @return the output location of the prepared spec
*/
const prepare = async ({
inputSpecLocation,
inputOptionsLocation,
outputSpecLocation,
...options
}: PrepareApiSpecCustomResourceProperties): Promise<S3Location> => {
// Read the spec from the s3 input location
const inputSpec = JSON.parse(
Expand All @@ -107,8 +110,19 @@ const prepare = async ({
).Body!.transformToString("utf-8")
);

const inputOptions: PrepareApiSpecOptions = JSON.parse(
await (
await s3.send(
new GetObjectCommand({
Bucket: inputOptionsLocation.bucket,
Key: inputOptionsLocation.key,
})
)
).Body!.transformToString("utf-8")
);

// Prepare the spec
const preparedSpec = prepareApiSpec(inputSpec, options);
const preparedSpec = prepareApiSpec(inputSpec, inputOptions);
const preparedSpecHash = crypto
.createHash("sha256")
.update(JSON.stringify(preparedSpec))
Expand Down
114 changes: 70 additions & 44 deletions packages/type-safe-api/src/construct/type-safe-rest-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
} from "aws-cdk-lib/aws-lambda";
import { LogGroup } from "aws-cdk-lib/aws-logs";
import { Asset } from "aws-cdk-lib/aws-s3-assets";
import { BucketDeployment, Source } from "aws-cdk-lib/aws-s3-deployment";
import {
CfnIPSet,
CfnWebACL,
Expand Down Expand Up @@ -120,11 +121,71 @@ export class TypeSafeRestApi extends Construct {
const inputSpecAsset = new Asset(this, "InputSpec", {
path: specPath,
});
// We'll output the prepared spec options in the same asset bucket
const preparedSpecOptionsKeyPrefix = `${inputSpecAsset.s3ObjectKey}-options`;

// We'll output the prepared spec in the same asset bucket
const preparedSpecOutputKeyPrefix = `${inputSpecAsset.s3ObjectKey}-prepared`;

const stack = Stack.of(this);

const serializedCorsOptions: SerializedCorsOptions | undefined =
corsOptions && {
allowHeaders: corsOptions.allowHeaders || [
...Cors.DEFAULT_HEADERS,
"x-amz-content-sha256",
],
allowMethods: corsOptions.allowMethods || Cors.ALL_METHODS,
allowOrigins: corsOptions.allowOrigins,
statusCode: corsOptions.statusCode || 204,
};

const prepareSpecOptions: PrepareApiSpecOptions = {
defaultAuthorizerReference:
serializeAsAuthorizerReference(defaultAuthorizer),
integrations: Object.fromEntries(
Object.entries(integrations).map(([operationId, integration]) => [
operationId,
{
integration: integration.integration.render({
operationId,
scope: this,
...operationLookup[operationId],
corsOptions: serializedCorsOptions,
operationLookup,
}),
methodAuthorizer: serializeAsAuthorizerReference(
integration.authorizer
),
options: integration.options,
},
])
),
securitySchemes: prepareSecuritySchemes(
this,
integrations,
defaultAuthorizer,
options.apiKeyOptions
),
corsOptions: serializedCorsOptions,
operationLookup,
apiKeyOptions: options.apiKeyOptions,
};

// Upload the configuration to s3 as a s3 deployment asset (support deploy-time values)
const inputOptionsDeployment = new BucketDeployment(
this,
"OptionsDeployment",
{
sources: [
Source.jsonData("preparedSpecOptions.json", prepareSpecOptions),
],
destinationBucket: inputSpecAsset.bucket,
destinationKeyPrefix: preparedSpecOptionsKeyPrefix,
prune: false,
}
);

// Lambda name prefix is truncated to 48 characters (16 below the max of 64)
const lambdaNamePrefix = `${PDKNag.getStackPrefix(stack)
.split("/")
Expand Down Expand Up @@ -157,6 +218,11 @@ export class TypeSafeRestApi extends Construct {
actions: ["s3:getObject"],
resources: [
inputSpecAsset.bucket.arnForObjects(inputSpecAsset.s3ObjectKey),
// The options file will include a hash of the prepared spec, which is not known until deploy time since
// tokens must be resolved
inputSpecAsset.bucket.arnForObjects(
`${preparedSpecOptionsKeyPrefix}/*`
),
],
}),
new PolicyStatement({
Expand Down Expand Up @@ -285,49 +351,6 @@ export class TypeSafeRestApi extends Construct {
}
);

const serializedCorsOptions: SerializedCorsOptions | undefined =
corsOptions && {
allowHeaders: corsOptions.allowHeaders || [
...Cors.DEFAULT_HEADERS,
"x-amz-content-sha256",
],
allowMethods: corsOptions.allowMethods || Cors.ALL_METHODS,
allowOrigins: corsOptions.allowOrigins,
statusCode: corsOptions.statusCode || 204,
};

const prepareSpecOptions: PrepareApiSpecOptions = {
defaultAuthorizerReference:
serializeAsAuthorizerReference(defaultAuthorizer),
integrations: Object.fromEntries(
Object.entries(integrations).map(([operationId, integration]) => [
operationId,
{
integration: integration.integration.render({
operationId,
scope: this,
...operationLookup[operationId],
corsOptions: serializedCorsOptions,
operationLookup,
}),
methodAuthorizer: serializeAsAuthorizerReference(
integration.authorizer
),
options: integration.options,
},
])
),
securitySchemes: prepareSecuritySchemes(
this,
integrations,
defaultAuthorizer,
options.apiKeyOptions
),
corsOptions: serializedCorsOptions,
operationLookup,
apiKeyOptions: options.apiKeyOptions,
};

// Spec preparation will happen in a custom resource lambda so that references to lambda integrations etc can be
// resolved. However, we also prepare inline to perform some additional validation at synth time.
const spec = JSON.parse(fs.readFileSync(specPath, "utf-8"));
Expand All @@ -339,11 +362,14 @@ export class TypeSafeRestApi extends Construct {
bucket: inputSpecAsset.bucket.bucketName,
key: inputSpecAsset.s3ObjectKey,
},
inputOptionsLocation: {
bucket: inputOptionsDeployment.deployedBucket.bucketName,
key: `${preparedSpecOptionsKeyPrefix}/preparedSpecOptions.json`,
},
outputSpecLocation: {
bucket: inputSpecAsset.bucket.bucketName,
key: preparedSpecOutputKeyPrefix,
},
...prepareSpecOptions,
};

const prepareSpecCustomResource = new CustomResource(
Expand Down
Loading