diff --git a/src/cdk/add-lambda-resource.ts b/src/cdk/add-lambda-resource.ts index 8d0cb05..728810b 100644 --- a/src/cdk/add-lambda-resource.ts +++ b/src/cdk/add-lambda-resource.ts @@ -1,4 +1,4 @@ -import type {LambdaRoute, StackConfig} from '../read-stack-config.js'; +import type {LambdaRoute, StackConfig} from '../parse-stack-config.js'; import type {Stack, aws_lambda} from 'aws-cdk-lib'; import {addCorsPreflight} from './add-cors-preflight.js'; diff --git a/src/cdk/add-s3-resource.ts b/src/cdk/add-s3-resource.ts index 9346d6d..65b0cf7 100644 --- a/src/cdk/add-s3-resource.ts +++ b/src/cdk/add-s3-resource.ts @@ -1,4 +1,4 @@ -import type {S3Route} from '../read-stack-config.js'; +import type {S3Route} from '../parse-stack-config.js'; import type {aws_iam, aws_s3} from 'aws-cdk-lib'; import {addCorsPreflight} from './add-cors-preflight.js'; diff --git a/src/cdk/create-lambda-function.ts b/src/cdk/create-lambda-function.ts index 3d75fbf..2901e42 100644 --- a/src/cdk/create-lambda-function.ts +++ b/src/cdk/create-lambda-function.ts @@ -1,4 +1,4 @@ -import type {LambdaRoute, StackConfig} from '../read-stack-config.js'; +import type {LambdaRoute, StackConfig} from '../parse-stack-config.js'; import type {Stack} from 'aws-cdk-lib'; import {getDomainName} from '../utils/get-domain-name.js'; diff --git a/src/cdk/create-request-authorizer.ts b/src/cdk/create-request-authorizer.ts index bdd2f17..fdcc888 100644 --- a/src/cdk/create-request-authorizer.ts +++ b/src/cdk/create-request-authorizer.ts @@ -1,4 +1,4 @@ -import type {StackConfig} from '../read-stack-config.js'; +import type {StackConfig} from '../parse-stack-config.js'; import type {Stack} from 'aws-cdk-lib'; import {getDomainName} from '../utils/get-domain-name.js'; diff --git a/src/cdk/create-rest-api.ts b/src/cdk/create-rest-api.ts index 4566f80..8223519 100644 --- a/src/cdk/create-rest-api.ts +++ b/src/cdk/create-rest-api.ts @@ -1,4 +1,4 @@ -import type {StackConfig} from '../read-stack-config.js'; +import type {StackConfig} from '../parse-stack-config.js'; import type {Stack} from 'aws-cdk-lib'; import {getDomainName} from '../utils/get-domain-name.js'; diff --git a/src/cdk/create-stack.ts b/src/cdk/create-stack.ts index b4d9254..9b02e0e 100644 --- a/src/cdk/create-stack.ts +++ b/src/cdk/create-stack.ts @@ -1,4 +1,4 @@ -import type {StackConfig} from '../read-stack-config.js'; +import type {StackConfig} from '../parse-stack-config.js'; import {getDomainName} from '../utils/get-domain-name.js'; import {getStackName} from '../utils/get-stack-name.js'; diff --git a/src/delete-command.ts b/src/delete-command.ts index 3d83fb7..4f4f079 100644 --- a/src/delete-command.ts +++ b/src/delete-command.ts @@ -1,5 +1,6 @@ import type {CommandModule} from 'yargs'; +import {parseDomainNameParts} from './parse-domain-name-parts.js'; import {readStackConfig} from './read-stack-config.js'; import {deleteStack} from './sdk/delete-stack.js'; import {getDomainName} from './utils/get-domain-name.js'; @@ -34,7 +35,10 @@ export const deleteCommand: CommandModule< handler: async (args): Promise => { const stackName = - args.stackName || getStackName(getDomainName(await readStackConfig())); + args.stackName || + getStackName( + getDomainName(parseDomainNameParts(await readStackConfig())), + ); print.warning(`Stack: ${stackName}`); diff --git a/src/dev/create-lambda-request-handler.ts b/src/dev/create-lambda-request-handler.ts index 1571f05..ef9c116 100644 --- a/src/dev/create-lambda-request-handler.ts +++ b/src/dev/create-lambda-request-handler.ts @@ -1,4 +1,4 @@ -import type {LambdaRoute} from '../read-stack-config.js'; +import type {LambdaRoute} from '../parse-stack-config.js'; import type {APIGatewayProxyResult} from 'aws-lambda'; import type express from 'express'; diff --git a/src/dev/get-router-matcher.ts b/src/dev/get-router-matcher.ts index cb71529..c39934e 100644 --- a/src/dev/get-router-matcher.ts +++ b/src/dev/get-router-matcher.ts @@ -1,4 +1,4 @@ -import type {LambdaRoute} from '../read-stack-config.js'; +import type {LambdaRoute} from '../parse-stack-config.js'; import type {Express, IRouterMatcher} from 'express'; export function getRouterMatcher( diff --git a/src/dev/register-s3-route.ts b/src/dev/register-s3-route.ts index 2c8aed8..283e5b5 100644 --- a/src/dev/register-s3-route.ts +++ b/src/dev/register-s3-route.ts @@ -1,4 +1,4 @@ -import type {S3Route} from '../read-stack-config.js'; +import type {S3Route} from '../parse-stack-config.js'; import type {Express} from 'express'; import express from 'express'; diff --git a/src/dev/sort-routes.ts b/src/dev/sort-routes.ts index 49f3bd0..2b0f645 100644 --- a/src/dev/sort-routes.ts +++ b/src/dev/sort-routes.ts @@ -1,4 +1,4 @@ -import type {Route} from '../read-stack-config.js'; +import type {Route} from '../parse-stack-config.js'; export function sortRoutes>( routes: readonly TRoute[], diff --git a/src/flush-cache-command.ts b/src/flush-cache-command.ts index 9334244..5467004 100644 --- a/src/flush-cache-command.ts +++ b/src/flush-cache-command.ts @@ -1,5 +1,6 @@ import type {CommandModule} from 'yargs'; +import {parseDomainNameParts} from './parse-domain-name-parts.js'; import {readStackConfig} from './read-stack-config.js'; import {findStack} from './sdk/find-stack.js'; import {flushRestApiCache} from './sdk/flush-rest-api-cache.js'; @@ -36,7 +37,10 @@ export const flushCacheCommand: CommandModule< handler: async (args): Promise => { const stackName = - args.stackName || getStackName(getDomainName(await readStackConfig())); + args.stackName || + getStackName( + getDomainName(parseDomainNameParts(await readStackConfig())), + ); print.warning(`Stack: ${stackName}`); diff --git a/src/index.ts b/src/index.ts index 73b1821..6ebdd21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import type { Route, S3Route, StackConfig, -} from './read-stack-config.js'; +} from './parse-stack-config.js'; import {cleanupCommand} from './cleanup-command.js'; import {deleteCommand} from './delete-command.js'; diff --git a/src/list-command.ts b/src/list-command.ts index 018b09d..e56d416 100644 --- a/src/list-command.ts +++ b/src/list-command.ts @@ -1,5 +1,6 @@ import type {CommandModule} from 'yargs'; +import {parseDomainNameParts} from './parse-domain-name-parts.js'; import {readStackConfig} from './read-stack-config.js'; import {findStacks} from './sdk/find-stacks.js'; import {getFormattedAgeInDays} from './utils/get-formatted-age-in-days.js'; @@ -53,7 +54,8 @@ export const listCommand: CommandModule< const hostedZoneName = all ? undefined - : args.hostedZoneName || (await readStackConfig()).hostedZoneName; + : args.hostedZoneName || + parseDomainNameParts(await readStackConfig()).hostedZoneName; if (!hostedZoneName && !all) { throw new Error( diff --git a/src/parse-domain-name-parts.ts b/src/parse-domain-name-parts.ts new file mode 100644 index 0000000..2c1bd27 --- /dev/null +++ b/src/parse-domain-name-parts.ts @@ -0,0 +1,12 @@ +import {z} from 'zod'; + +export type DomainNameParts = z.TypeOf; + +export const DomainNamePartsSchema = z.object({ + hostedZoneName: z.string().optional(), + aliasRecordName: z.string().optional(), +}); + +export function parseDomainNameParts(config: unknown): DomainNameParts { + return DomainNamePartsSchema.parse(config); +} diff --git a/src/parse-stack-config.ts b/src/parse-stack-config.ts new file mode 100644 index 0000000..b89c76f --- /dev/null +++ b/src/parse-stack-config.ts @@ -0,0 +1,111 @@ +import type {Stack, aws_apigateway, aws_lambda} from 'aws-cdk-lib'; +import type {Express} from 'express'; + +import {DomainNamePartsSchema} from './parse-domain-name-parts.js'; +import {validateRoutes} from './utils/validate-routes.js'; +import {z} from 'zod'; + +export type StackConfig = Omit< + z.TypeOf, + 'routes' | 'onSynthesize' | 'onStart' +> & { + readonly routes: Route[]; + + readonly onSynthesize?: (constructs: { + readonly stack: Stack; + readonly restApi: aws_apigateway.RestApiBase; + }) => void; + + readonly onStart?: (app: Express) => void; +}; + +export type Route = LambdaRoute | S3Route; + +export type LambdaRoute = Omit< + z.TypeOf, + 'onSynthesize' +> & { + readonly onSynthesize?: (constructs: { + readonly stack: Stack; + readonly restApi: aws_apigateway.RestApiBase; + readonly lambdaFunction: aws_lambda.FunctionBase; + }) => void; +}; + +export type S3Route = z.TypeOf; + +const LambdaRouteSchema = z.object({ + type: z.literal(`function`), + httpMethod: z.enum([`DELETE`, `GET`, `HEAD`, `PATCH`, `POST`, `PUT`]), + publicPath: z.string(), + path: z.string(), + functionName: z.string(), + memorySize: z.number().optional(), + timeoutInSeconds: z.number().int().min(0).max(28).optional(), + environment: z.record(z.string()).optional(), + requestParameters: z + .record( + z.object({ + cacheKey: z.boolean().optional(), + required: z.boolean().optional(), + }), + ) + .optional(), + throttling: z + .object({rateLimit: z.number(), burstLimit: z.number()}) + .optional(), + cacheTtlInSeconds: z.number().int().min(0).max(3600).optional(), + authenticationEnabled: z.boolean().optional(), + corsEnabled: z.boolean().optional(), + onSynthesize: z.function().optional(), +}); + +const S3RouteSchema = z.object({ + type: z.enum([`file`, `folder`]), + httpMethod: z.literal(`GET`).optional(), + publicPath: z.string(), + path: z.string(), + responseHeaders: z.record(z.string()).optional(), + throttling: z + .object({rateLimit: z.number(), burstLimit: z.number()}) + .optional(), + cacheTtlInSeconds: z.number().int().min(0).max(3600).optional(), + authenticationEnabled: z.boolean().optional(), + corsEnabled: z.boolean().optional(), +}); + +const StackConfigSchema = DomainNamePartsSchema.extend({ + cachingEnabled: z.boolean().optional(), + terminationProtectionEnabled: z.boolean().optional(), + authentication: z + .object({ + username: z.string(), + password: z.string(), + realm: z.string().optional(), + cacheTtlInSeconds: z.number().int().min(0).max(3600).optional(), + }) + .optional(), + monitoring: z + .union([ + z.literal(true), + z.object({ + accessLoggingEnabled: z.boolean().optional(), + loggingEnabled: z.boolean().optional(), + metricsEnabled: z.boolean().optional(), + tracingEnabled: z.boolean().optional(), + }), + ]) + .optional(), + tags: z.record(z.string()).optional(), + routes: z.array(z.union([LambdaRouteSchema, S3RouteSchema])).min(1), + onSynthesize: z.function().optional(), + onStart: z.function().optional(), +}); + +export function parseStackConfig(config: unknown): StackConfig { + const stackConfig = StackConfigSchema.parse(config); + + validateRoutes(stackConfig.routes); + + return stackConfig; +} diff --git a/src/purge-command.ts b/src/purge-command.ts index 98abb23..8196d06 100644 --- a/src/purge-command.ts +++ b/src/purge-command.ts @@ -1,6 +1,7 @@ import type {Stack, Tag} from '@aws-sdk/client-cloudformation'; import type {CommandModule} from 'yargs'; +import {parseDomainNameParts} from './parse-domain-name-parts.js'; import {readStackConfig} from './read-stack-config.js'; import {deleteStack} from './sdk/delete-stack.js'; import {findStacks} from './sdk/find-stacks.js'; @@ -53,7 +54,8 @@ export const purgeCommand: CommandModule< handler: async (args): Promise => { const hostedZoneName = - args.hostedZoneName || (await readStackConfig()).hostedZoneName; + args.hostedZoneName || + parseDomainNameParts(await readStackConfig()).hostedZoneName; if (!hostedZoneName) { throw new Error(`Please specify a hosted zone name.`); diff --git a/src/read-stack-config.ts b/src/read-stack-config.ts index b12cfbe..f3af926 100644 --- a/src/read-stack-config.ts +++ b/src/read-stack-config.ts @@ -1,111 +1,7 @@ -import type {Stack, aws_apigateway, aws_lambda} from 'aws-cdk-lib'; -import type {Express} from 'express'; - -import {validateRoutes} from './utils/validate-routes.js'; import {resolve} from 'path'; import {pathToFileURL} from 'url'; -import {z} from 'zod'; - -export type StackConfig = Omit< - z.TypeOf, - 'routes' | 'onSynthesize' | 'onStart' -> & { - readonly routes: Route[]; - - readonly onSynthesize?: (constructs: { - readonly stack: Stack; - readonly restApi: aws_apigateway.RestApiBase; - }) => void; - - readonly onStart?: (app: Express) => void; -}; - -export type Route = LambdaRoute | S3Route; - -export type LambdaRoute = Omit< - z.TypeOf, - 'onSynthesize' -> & { - readonly onSynthesize?: (constructs: { - readonly stack: Stack; - readonly restApi: aws_apigateway.RestApiBase; - readonly lambdaFunction: aws_lambda.FunctionBase; - }) => void; -}; -export type S3Route = z.TypeOf; - -const LambdaRouteSchema = z.object({ - type: z.literal(`function`), - httpMethod: z.enum([`DELETE`, `GET`, `HEAD`, `PATCH`, `POST`, `PUT`]), - publicPath: z.string(), - path: z.string(), - functionName: z.string(), - memorySize: z.number().optional(), - timeoutInSeconds: z.number().int().min(0).max(28).optional(), - environment: z.record(z.string()).optional(), - requestParameters: z - .record( - z.object({ - cacheKey: z.boolean().optional(), - required: z.boolean().optional(), - }), - ) - .optional(), - throttling: z - .object({rateLimit: z.number(), burstLimit: z.number()}) - .optional(), - cacheTtlInSeconds: z.number().int().min(0).max(3600).optional(), - authenticationEnabled: z.boolean().optional(), - corsEnabled: z.boolean().optional(), - onSynthesize: z.function().optional(), -}); - -const S3RouteSchema = z.object({ - type: z.enum([`file`, `folder`]), - httpMethod: z.literal(`GET`).optional(), - publicPath: z.string(), - path: z.string(), - responseHeaders: z.record(z.string()).optional(), - throttling: z - .object({rateLimit: z.number(), burstLimit: z.number()}) - .optional(), - cacheTtlInSeconds: z.number().int().min(0).max(3600).optional(), - authenticationEnabled: z.boolean().optional(), - corsEnabled: z.boolean().optional(), -}); - -const StackConfigSchema = z.object({ - hostedZoneName: z.string().optional(), - aliasRecordName: z.string().optional(), - cachingEnabled: z.boolean().optional(), - terminationProtectionEnabled: z.boolean().optional(), - authentication: z - .object({ - username: z.string(), - password: z.string(), - realm: z.string().optional(), - cacheTtlInSeconds: z.number().int().min(0).max(3600).optional(), - }) - .optional(), - monitoring: z - .union([ - z.literal(true), - z.object({ - accessLoggingEnabled: z.boolean().optional(), - loggingEnabled: z.boolean().optional(), - metricsEnabled: z.boolean().optional(), - tracingEnabled: z.boolean().optional(), - }), - ]) - .optional(), - tags: z.record(z.string()).optional(), - routes: z.array(z.union([LambdaRouteSchema, S3RouteSchema])).min(1), - onSynthesize: z.function().optional(), - onStart: z.function().optional(), -}); - -export async function readStackConfig(port?: number): Promise { +export async function readStackConfig(port?: number): Promise { let module; const path = resolve(`aws-simple.config.mjs`); @@ -123,9 +19,5 @@ export async function readStackConfig(port?: number): Promise { ); } - const stackConfig = StackConfigSchema.parse(module.default(port)); - - validateRoutes(stackConfig.routes); - - return stackConfig; + return module.default(port); } diff --git a/src/redeploy-command.ts b/src/redeploy-command.ts index 3215269..d325bea 100644 --- a/src/redeploy-command.ts +++ b/src/redeploy-command.ts @@ -1,5 +1,6 @@ import type {CommandModule} from 'yargs'; +import {parseDomainNameParts} from './parse-domain-name-parts.js'; import {readStackConfig} from './read-stack-config.js'; import {findStack} from './sdk/find-stack.js'; import {getOutputValue} from './sdk/get-output-value.js'; @@ -36,7 +37,10 @@ export const redeployCommand: CommandModule< handler: async (args): Promise => { const stackName = - args.stackName || getStackName(getDomainName(await readStackConfig())); + args.stackName || + getStackName( + getDomainName(parseDomainNameParts(await readStackConfig())), + ); print.warning(`Stack: ${stackName}`); diff --git a/src/start-command.ts b/src/start-command.ts index e9128e4..47dd485 100644 --- a/src/start-command.ts +++ b/src/start-command.ts @@ -1,4 +1,4 @@ -import type {LambdaRoute} from './read-stack-config.js'; +import type {LambdaRoute} from './parse-stack-config.js'; import type {APIGatewayProxyResult} from 'aws-lambda'; import type {CommandModule} from 'yargs'; @@ -7,6 +7,7 @@ import {getRouterMatcher} from './dev/get-router-matcher.js'; import {registerS3Route} from './dev/register-s3-route.js'; import {removeAllRoutes} from './dev/remove-all-routes.js'; import {sortRoutes} from './dev/sort-routes.js'; +import {parseStackConfig} from './parse-stack-config.js'; import {readStackConfig} from './read-stack-config.js'; import {print} from './utils/print.js'; import {watch} from 'chokidar'; @@ -43,7 +44,7 @@ export const startCommand: CommandModule<{}, {readonly port: number}> = { app.use(compression({threshold: 150})); app.set(`etag`, false); - const stackConfig = await readStackConfig(port); + const stackConfig = parseStackConfig(await readStackConfig(port)); stackConfig.onStart?.(app); diff --git a/src/synthesize-command.ts b/src/synthesize-command.ts index be0a966..f41c35d 100644 --- a/src/synthesize-command.ts +++ b/src/synthesize-command.ts @@ -7,6 +7,7 @@ import {createBucket} from './cdk/create-bucket.js'; import {createRequestAuthorizer} from './cdk/create-request-authorizer.js'; import {createRestApi} from './cdk/create-rest-api.js'; import {createStack} from './cdk/create-stack.js'; +import {parseStackConfig} from './parse-stack-config.js'; import {readStackConfig} from './read-stack-config.js'; const commandName = `synthesize`; @@ -24,7 +25,7 @@ export const synthesizeCommand: CommandModule<{}, {}> = { ]), handler: async (): Promise => { - const stackConfig = await readStackConfig(); + const stackConfig = parseStackConfig(await readStackConfig()); const stack = createStack(stackConfig); const restApi = createRestApi(stackConfig, stack); const bucket = createBucket(stack); diff --git a/src/tag-command.ts b/src/tag-command.ts index 077cdde..d9cef61 100644 --- a/src/tag-command.ts +++ b/src/tag-command.ts @@ -1,5 +1,6 @@ import type {CommandModule} from 'yargs'; +import {parseDomainNameParts} from './parse-domain-name-parts.js'; import {readStackConfig} from './read-stack-config.js'; import {updateTags} from './sdk/update-tags.js'; import {getDomainName} from './utils/get-domain-name.js'; @@ -53,7 +54,10 @@ export const tagCommand: CommandModule< handler: async (args): Promise => { const stackName = - args.stackName || getStackName(getDomainName(await readStackConfig())); + args.stackName || + getStackName( + getDomainName(parseDomainNameParts(await readStackConfig())), + ); print.warning(`Stack: ${stackName}`); diff --git a/src/upload-command.ts b/src/upload-command.ts index 510fb10..e104023 100644 --- a/src/upload-command.ts +++ b/src/upload-command.ts @@ -1,5 +1,6 @@ import type {CommandModule} from 'yargs'; +import {parseStackConfig} from './parse-stack-config.js'; import {readStackConfig} from './read-stack-config.js'; import {findStack} from './sdk/find-stack.js'; import {getOutputValue} from './sdk/get-output-value.js'; @@ -25,7 +26,7 @@ export const uploadCommand: CommandModule<{}, {readonly yes: boolean}> = { .example([[`npx $0 ${commandName}`], [`npx $0 ${commandName} --yes`]]), handler: async (args): Promise => { - const stackConfig = await readStackConfig(); + const stackConfig = parseStackConfig(await readStackConfig()); const domainName = getDomainName(stackConfig); const stackName = getStackName(domainName); diff --git a/src/utils/get-domain-name.ts b/src/utils/get-domain-name.ts index ab1b026..81ff45f 100644 --- a/src/utils/get-domain-name.ts +++ b/src/utils/get-domain-name.ts @@ -1,7 +1,4 @@ -export interface DomainNameParts { - readonly hostedZoneName?: string; - readonly aliasRecordName?: string; -} +import type {DomainNameParts} from '../parse-domain-name-parts.js'; export function getDomainName(parts: DomainNameParts): string { const {hostedZoneName, aliasRecordName} = parts; diff --git a/src/utils/validate-routes.test.ts b/src/utils/validate-routes.test.ts index 77f64b0..1c31d58 100644 --- a/src/utils/validate-routes.test.ts +++ b/src/utils/validate-routes.test.ts @@ -1,4 +1,4 @@ -import type {LambdaRoute, S3Route} from '../read-stack-config.js'; +import type {LambdaRoute, S3Route} from '../parse-stack-config.js'; import {validateRoutes} from './validate-routes.js'; import {describe, expect, test} from '@jest/globals'; diff --git a/src/utils/validate-routes.ts b/src/utils/validate-routes.ts index 5c629cf..e11ae3a 100644 --- a/src/utils/validate-routes.ts +++ b/src/utils/validate-routes.ts @@ -1,4 +1,4 @@ -import type {Route} from '../read-stack-config.js'; +import type {Route} from '../parse-stack-config.js'; import {getNormalizedName} from './get-normalized-name.js';