diff --git a/.github/workflows/pr.test.yaml b/.github/workflows/pr.test.yaml index a899438..a60659d 100644 --- a/.github/workflows/pr.test.yaml +++ b/.github/workflows/pr.test.yaml @@ -41,7 +41,13 @@ jobs: run: pnpm install bun -g - name: Install - run: pnpm install --frozen-lockfile --strict-peer-dependencies + run: pnpm install + + - name: Build + run: bun run build + + - name: Install local bin scripts + run: pnpm install - name: Test run: bun test diff --git a/package.json b/package.json index d2b3f44..98cb323 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "private": true, "scripts": { - "build": "turbo run dev", + "build": "turbo run build", "dev": "turbo run dev", "test": ": 'run `$ bun test` instead'", "format": "prettier --write .", diff --git a/packages/aws.iac/package.json b/packages/aws.iac/package.json new file mode 100644 index 0000000..8caa5fb --- /dev/null +++ b/packages/aws.iac/package.json @@ -0,0 +1,29 @@ +{ + "type": "module", + "name": "@notation/aws.iac", + "version": "0.0.1", + "scripts": { + "build": "tsup", + "dev": "npm run build -- --watch" + }, + "exports": { + "./client": { + "import": "./dist/client.js", + "types": "./dist/client.d.ts" + }, + "./resources": { + "import": "./dist/resources.js", + "types": "./dist/resources.d.ts" + } + }, + "dependencies": { + "@aws-sdk/client-sts": "^3.441.0", + "@aws-sdk/client-apigatewayv2": "^3.441.0", + "@aws-sdk/client-lambda": "^3.441.0", + "@aws-sdk/client-iam": "^3.441.0", + "@aws-sdk/client-cloudwatch-logs": "^3.441.0", + "@notation/core": "workspace:*", + "@notation/std.iac": "workspace:*", + "@types/aws-lambda": "^8.10.125" + } +} diff --git a/packages/aws.iac/src/client.ts b/packages/aws.iac/src/client.ts new file mode 100644 index 0000000..15be9f9 --- /dev/null +++ b/packages/aws.iac/src/client.ts @@ -0,0 +1,5 @@ +import { ResourceGroup } from "@notation/core"; + +export class AwsResourceGroup extends ResourceGroup { + platform = "aws"; +} diff --git a/packages/aws.iac/src/config.ts b/packages/aws.iac/src/config.ts new file mode 100644 index 0000000..09c3154 --- /dev/null +++ b/packages/aws.iac/src/config.ts @@ -0,0 +1 @@ +export const region = "us-west-2"; diff --git a/packages/aws.iac/src/context.ts b/packages/aws.iac/src/context.ts new file mode 100644 index 0000000..e57626e --- /dev/null +++ b/packages/aws.iac/src/context.ts @@ -0,0 +1,9 @@ +import { GetCallerIdentityCommand } from "@aws-sdk/client-sts"; +import { stsClient } from "src/utils/aws-clients"; + +const command = new GetCallerIdentityCommand({}); + +export const getAwsAccountId = async () => { + const response = await stsClient.send(command); + return response.Account; +}; diff --git a/packages/aws.iac/src/resources.ts b/packages/aws.iac/src/resources.ts new file mode 100644 index 0000000..e00d29a --- /dev/null +++ b/packages/aws.iac/src/resources.ts @@ -0,0 +1,2 @@ +export * as apiGateway from "./resources/api-gateway"; +export * as lambda from "./resources/lambda"; diff --git a/packages/aws.iac/src/resources/api-gateway/api.ts b/packages/aws.iac/src/resources/api-gateway/api.ts new file mode 100644 index 0000000..8946ef5 --- /dev/null +++ b/packages/aws.iac/src/resources/api-gateway/api.ts @@ -0,0 +1,23 @@ +import { createResourceFactory } from "@notation/core"; +import { + CreateApiCommand, + CreateApiCommandInput, + CreateApiCommandOutput, +} from "@aws-sdk/client-apigatewayv2"; +import { apiGatewayClient } from "src/utils/aws-clients"; + +export type ApiInput = CreateApiCommandInput; +export type ApiOutput = CreateApiCommandOutput; + +const createApiClass = createResourceFactory(); + +export const Api = createApiClass({ + type: "aws/api-gateway", + + async deploy(props: ApiInput) { + const command = new CreateApiCommand(props); + return apiGatewayClient.send(command); + }, +}); + +export type ApiInstance = InstanceType; diff --git a/packages/aws.iac/src/resources/api-gateway/index.ts b/packages/aws.iac/src/resources/api-gateway/index.ts new file mode 100644 index 0000000..055ce66 --- /dev/null +++ b/packages/aws.iac/src/resources/api-gateway/index.ts @@ -0,0 +1,4 @@ +export * from "./api"; +export * from "./lambda-integration"; +export * from "./route"; +export * from "./stage"; diff --git a/packages/aws.iac/src/resources/api-gateway/lambda-integration.ts b/packages/aws.iac/src/resources/api-gateway/lambda-integration.ts new file mode 100644 index 0000000..ceadb49 --- /dev/null +++ b/packages/aws.iac/src/resources/api-gateway/lambda-integration.ts @@ -0,0 +1,46 @@ +import { createResourceFactory } from "@notation/core"; +import { + CreateIntegrationCommand, + CreateIntegrationCommandInput, + CreateIntegrationCommandOutput, +} from "@aws-sdk/client-apigatewayv2"; +import { ApiInstance } from "./api"; +import { LambdaInstance } from "../lambda"; +import { getLambdaInvocationUri } from "src/templates/arn"; +import { apiGatewayClient } from "src/utils/aws-clients"; + +export type LambdaIntegrationInput = CreateIntegrationCommandInput; +export type LambdaIntegrationOutput = CreateIntegrationCommandOutput; +export type LambdaIntegrationDependencies = { + api: ApiInstance; + lambda: LambdaInstance; +}; + +const createLambdaIntegrationClass = createResourceFactory< + LambdaIntegrationInput, + LambdaIntegrationOutput, + LambdaIntegrationDependencies +>(); + +export const LambdaIntegration = createLambdaIntegrationClass({ + type: "aws/api-gateway/integration/lambda", + + getIntrinsicConfig: (dependencies) => ({ + ApiId: dependencies.api.output.ApiId, + IntegrationType: "AWS_PROXY", + IntegrationMethod: "POST", + IntegrationUri: getLambdaInvocationUri( + dependencies.lambda.output.FunctionArn!, + ), + PayloadFormatVersion: "2.0", + PassthroughBehavior: "WHEN_NO_MATCH", + ConnectionType: "INTERNET", + }), + + deploy: async (input) => { + const command = new CreateIntegrationCommand(input); + return apiGatewayClient.send(command); + }, +}); + +export type LambdaIntegrationInstance = InstanceType; diff --git a/packages/aws.iac/src/resources/api-gateway/route.ts b/packages/aws.iac/src/resources/api-gateway/route.ts new file mode 100644 index 0000000..673a321 --- /dev/null +++ b/packages/aws.iac/src/resources/api-gateway/route.ts @@ -0,0 +1,37 @@ +import { createResourceFactory } from "@notation/core"; +import { + CreateRouteCommand, + CreateRouteCommandInput, + CreateRouteCommandOutput, +} from "@aws-sdk/client-apigatewayv2"; +import { apiGatewayClient } from "src/utils/aws-clients"; +import { ApiInstance, LambdaIntegrationInstance } from "."; + +export type RouteInput = CreateRouteCommandInput; +export type RouteOutput = CreateRouteCommandOutput; +export type RouteDeps = { + api: ApiInstance; + lambdaIntegration: LambdaIntegrationInstance; +}; + +const createRouteClass = createResourceFactory< + RouteInput, + RouteOutput, + RouteDeps +>(); + +export const Route = createRouteClass({ + type: "aws/api-gateway/route", + + getIntrinsicConfig: (dependencies) => ({ + ApiId: dependencies.api.output.ApiId, + Target: `integrations/${dependencies.lambdaIntegration.output.IntegrationId}`, + }), + + deploy: async (props: RouteInput) => { + const command = new CreateRouteCommand(props); + return apiGatewayClient.send(command); + }, +}); + +export type RouteInstance = InstanceType; diff --git a/packages/aws.iac/src/resources/api-gateway/stage.ts b/packages/aws.iac/src/resources/api-gateway/stage.ts new file mode 100644 index 0000000..653924c --- /dev/null +++ b/packages/aws.iac/src/resources/api-gateway/stage.ts @@ -0,0 +1,33 @@ +import { createResourceFactory } from "@notation/core"; +import { + CreateStageCommand, + CreateStageCommandInput, + CreateStageCommandOutput, +} from "@aws-sdk/client-apigatewayv2"; +import { apiGatewayClient } from "src/utils/aws-clients"; +import { Api } from "./api"; + +export type StageInput = CreateStageCommandInput; +export type StageOutput = CreateStageCommandOutput; +export type StageDependencies = { router: InstanceType }; + +const createStageClass = createResourceFactory< + StageInput, + StageOutput, + StageDependencies +>(); + +export const Stage = createStageClass({ + type: "aws/api-gateway/stage", + + getIntrinsicConfig: (dependencies) => ({ + ApiId: dependencies.router.output.ApiId, + }), + + async deploy(props: StageInput) { + const command = new CreateStageCommand(props); + return apiGatewayClient.send(command); + }, +}); + +export type StageInstance = InstanceType; diff --git a/packages/aws.iac/src/resources/lambda/index.ts b/packages/aws.iac/src/resources/lambda/index.ts new file mode 100644 index 0000000..b1eed19 --- /dev/null +++ b/packages/aws.iac/src/resources/lambda/index.ts @@ -0,0 +1,5 @@ +export * from "./lambda-api-permission"; +export * from "./lambda-log-group"; +export * from "./lambda-role-policy-attachment"; +export * from "./lambda-role"; +export * from "./lambda"; diff --git a/packages/aws.iac/src/resources/lambda/lambda-api-permission.ts b/packages/aws.iac/src/resources/lambda/lambda-api-permission.ts new file mode 100644 index 0000000..ac80ba3 --- /dev/null +++ b/packages/aws.iac/src/resources/lambda/lambda-api-permission.ts @@ -0,0 +1,48 @@ +import { createResourceFactory } from "@notation/core"; +import { + AddPermissionCommand, + AddPermissionCommandInput, + AddPermissionCommandOutput, +} from "@aws-sdk/client-lambda"; +import { lambdaClient } from "src/utils/aws-clients"; +import { generateApiGatewaySourceArn } from "src/templates/arn"; +import { ApiInstance } from "src/resources/api-gateway/api"; +import { LambdaInstance } from "./lambda"; + +export type LambdaApiGatewayPermissionInput = AddPermissionCommandInput; +export type LambdaApiGatewayPermissionOutput = AddPermissionCommandOutput; +export type LambdaApiGatewayPermissionDependencies = { + lambda: LambdaInstance; + api: ApiInstance; +}; + +const createLambdaApiGatewayPermissionClass = createResourceFactory< + LambdaApiGatewayPermissionInput, + LambdaApiGatewayPermissionOutput, + LambdaApiGatewayPermissionDependencies +>(); + +export const LambdaApiGatewayPermission = createLambdaApiGatewayPermissionClass( + { + type: "aws/lambda/permission/api-gateway", + + getIntrinsicConfig: async (dependencies) => ({ + StatementId: "AllowExecutionFromAPIGateway", + Principal: "apigateway.amazonaws.com", + FunctionName: dependencies.lambda.output.FunctionName, + Action: "lambda:InvokeFunction", + SourceArn: await generateApiGatewaySourceArn( + dependencies.api.output.ApiId!, + ), + }), + + deploy: async (config: LambdaApiGatewayPermissionInput) => { + const command = new AddPermissionCommand(config); + return lambdaClient.send(command); + }, + }, +); + +export type LambdaApiGatewayPermissionInstance = InstanceType< + typeof LambdaApiGatewayPermission +>; diff --git a/packages/aws.iac/src/resources/lambda/lambda-log-group.ts b/packages/aws.iac/src/resources/lambda/lambda-log-group.ts new file mode 100644 index 0000000..c40949d --- /dev/null +++ b/packages/aws.iac/src/resources/lambda/lambda-log-group.ts @@ -0,0 +1,33 @@ +import { createResourceFactory } from "@notation/core"; +import { + CreateLogGroupCommand, + CreateLogGroupCommandInput, + CreateLogGroupCommandOutput, +} from "@aws-sdk/client-cloudwatch-logs"; +import { cloudWatchLogsClient } from "src/utils/aws-clients"; +import { LambdaInstance } from "./lambda"; + +export type LambdaLogGroupInput = CreateLogGroupCommandInput; +export type LambdaLogGroupOutput = CreateLogGroupCommandOutput; +export type LambdaLogGroupDeps = { lambda: LambdaInstance }; + +const createLambdaLogGroupClass = createResourceFactory< + LambdaLogGroupInput, + LambdaLogGroupOutput, + LambdaLogGroupDeps +>(); + +export const LambdaLogGroup = createLambdaLogGroupClass({ + type: "aws/lambda/log-group", + + getIntrinsicConfig: (dependencies) => ({ + logGroupName: `/aws/lambda/${dependencies.lambda.output.FunctionName}`, + }), + + deploy: async (props: LambdaLogGroupInput) => { + const command = new CreateLogGroupCommand(props); + return cloudWatchLogsClient.send(command); + }, +}); + +export type LambdaLogGroupInstance = InstanceType; diff --git a/packages/aws.iac/src/resources/lambda/lambda-role-policy-attachment.ts b/packages/aws.iac/src/resources/lambda/lambda-role-policy-attachment.ts new file mode 100644 index 0000000..85e6350 --- /dev/null +++ b/packages/aws.iac/src/resources/lambda/lambda-role-policy-attachment.ts @@ -0,0 +1,37 @@ +import { createResourceFactory } from "@notation/core"; +import { + AttachRolePolicyCommand, + AttachRolePolicyCommandInput, + AttachRolePolicyCommandOutput, +} from "@aws-sdk/client-iam"; +import { iamClient } from "src/utils/aws-clients"; +import { LambdaIamRoleInstance } from "./lambda-role"; + +export type LambdaRolePolicyAttachmentInput = AttachRolePolicyCommandInput; +export type LambdaRolePolicyAttachmentOutput = AttachRolePolicyCommandOutput; +export type LambdaRolePolicyAttachmentDeps = { role: LambdaIamRoleInstance }; + +const createLambdaRolePolicyAttachmentClass = createResourceFactory< + LambdaRolePolicyAttachmentInput, + LambdaRolePolicyAttachmentOutput, + LambdaRolePolicyAttachmentDeps +>(); + +export const LambdaRolePolicyAttachment = createLambdaRolePolicyAttachmentClass( + { + type: "aws/lambda/policy-attachment", + + getIntrinsicConfig: (dependencies) => ({ + RoleName: dependencies.role.output.Role!.RoleName, + }), + + deploy: async (props: LambdaRolePolicyAttachmentInput) => { + const command = new AttachRolePolicyCommand(props); + return iamClient.send(command); + }, + }, +); + +export type LambdaRolePolicyAttachmentInstance = InstanceType< + typeof LambdaRolePolicyAttachment +>; diff --git a/packages/aws.iac/src/resources/lambda/lambda-role.ts b/packages/aws.iac/src/resources/lambda/lambda-role.ts new file mode 100644 index 0000000..60bb4e8 --- /dev/null +++ b/packages/aws.iac/src/resources/lambda/lambda-role.ts @@ -0,0 +1,31 @@ +import { createResourceFactory } from "@notation/core"; +import { + CreateRoleCommand, + CreateRoleCommandInput, + CreateRoleCommandOutput, +} from "@aws-sdk/client-iam"; +import { iamClient } from "src/utils/aws-clients"; +import { lambdaTrustPolicy } from "src/templates/iam.policy"; + +export type LambdaIamRoleInput = CreateRoleCommandInput; +export type LambdaIamRoleOutput = CreateRoleCommandOutput; + +const createLambdaIamRoleClass = createResourceFactory< + LambdaIamRoleInput, + LambdaIamRoleOutput +>(); + +export const LambdaIamRole = createLambdaIamRoleClass({ + type: "aws/lambda/role", + + getIntrinsicConfig: () => ({ + AssumeRolePolicyDocument: JSON.stringify(lambdaTrustPolicy), + }), + + deploy: async (props: LambdaIamRoleInput) => { + const command = new CreateRoleCommand(props); + return iamClient.send(command); + }, +}); + +export type LambdaIamRoleInstance = InstanceType; diff --git a/packages/aws.iac/src/resources/lambda/lambda.ts b/packages/aws.iac/src/resources/lambda/lambda.ts new file mode 100644 index 0000000..0308ab6 --- /dev/null +++ b/packages/aws.iac/src/resources/lambda/lambda.ts @@ -0,0 +1,46 @@ +import { createResourceFactory } from "@notation/core"; +import { + CreateFunctionCommand, + CreateFunctionCommandInput, + CreateFunctionCommandOutput, +} from "@aws-sdk/client-lambda"; +import { LambdaIamRoleInstance, LambdaRolePolicyAttachmentInstance } from "./"; +import { ZipFileInstance } from "@notation/std.iac"; +import { lambdaClient } from "src/utils/aws-clients"; + +export type LambdaInput = CreateFunctionCommandInput; +export type LambdaOutput = CreateFunctionCommandOutput; + +type LambdaImplicitDeps = { + policyAttachment: LambdaRolePolicyAttachmentInstance; +}; + +export type LambdaDeps = { + role: LambdaIamRoleInstance; + zipFile: ZipFileInstance; +}; + +const createLambdaClass = createResourceFactory< + LambdaInput, + LambdaOutput, + LambdaDeps & LambdaImplicitDeps +>(); + +export const Lambda = createLambdaClass({ + type: "aws/lambda", + + getIntrinsicConfig: (dependencies) => ({ + PackageType: "Zip", + Code: { ZipFile: dependencies.zipFile.output.contents }, + Role: dependencies.role.output.Role!.Arn, + }), + + deploy: async (props: LambdaInput) => { + const command = new CreateFunctionCommand(props); + return lambdaClient.send(command); + }, + + retryOn: ["InvalidParameterValueException"], +}); + +export type LambdaInstance = InstanceType; diff --git a/packages/aws.iac/src/templates/arn.ts b/packages/aws.iac/src/templates/arn.ts new file mode 100644 index 0000000..4988e33 --- /dev/null +++ b/packages/aws.iac/src/templates/arn.ts @@ -0,0 +1,11 @@ +import { getAwsAccountId } from "src/context"; +import { region } from "src/config"; + +export const getLambdaInvocationUri = (arn: string) => { + return `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations`; +}; + +export const generateApiGatewaySourceArn = async (apiId: string) => { + const accountId = await getAwsAccountId(); + return `arn:aws:execute-api:${region}:${accountId}:${apiId}/*/*`; +}; diff --git a/packages/aws.iac/src/templates/iam.assume-role.ts b/packages/aws.iac/src/templates/iam.assume-role.ts new file mode 100644 index 0000000..65e048d --- /dev/null +++ b/packages/aws.iac/src/templates/iam.assume-role.ts @@ -0,0 +1,13 @@ +export function assumeRolePolicyForPrincipal(principal: string) { + return { + Version: "2012-10-17", + Statement: [ + { + Sid: "AllowAssumeRole", + Effect: "Allow", + Principal: principal, + Action: "sts:AssumeRole", + }, + ], + }; +} diff --git a/packages/aws.iac/src/templates/iam.managed-policy.ts b/packages/aws.iac/src/templates/iam.managed-policy.ts new file mode 100644 index 0000000..060e11f --- /dev/null +++ b/packages/aws.iac/src/templates/iam.managed-policy.ts @@ -0,0 +1,2 @@ +export const AWSLambdaBasicExecutionRole = + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"; diff --git a/packages/aws.iac/src/templates/iam.policy.ts b/packages/aws.iac/src/templates/iam.policy.ts new file mode 100644 index 0000000..fc0250a --- /dev/null +++ b/packages/aws.iac/src/templates/iam.policy.ts @@ -0,0 +1,13 @@ +export const lambdaTrustPolicy = { + Version: "2012-10-17", + Statement: [ + { + Sid: "AllowAssumeRole", + Effect: "Allow", + Principal: { + Service: "lambda.amazonaws.com", + }, + Action: "sts:AssumeRole", + }, + ], +}; diff --git a/packages/aws.iac/src/utils/aws-clients.ts b/packages/aws.iac/src/utils/aws-clients.ts new file mode 100644 index 0000000..1972340 --- /dev/null +++ b/packages/aws.iac/src/utils/aws-clients.ts @@ -0,0 +1,12 @@ +import { STSClient } from "@aws-sdk/client-sts"; +import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs"; +import { ApiGatewayV2Client } from "@aws-sdk/client-apigatewayv2"; +import { LambdaClient } from "@aws-sdk/client-lambda"; +import { IAMClient } from "@aws-sdk/client-iam"; +import { region } from "src/config"; + +export const stsClient = new STSClient({ region }); +export const cloudWatchLogsClient = new CloudWatchLogsClient({ region }); +export const lambdaClient = new LambdaClient({ region }); +export const apiGatewayClient = new ApiGatewayV2Client({ region }); +export const iamClient = new IAMClient({ region }); diff --git a/packages/aws.iac/tsconfig.json b/packages/aws.iac/tsconfig.json new file mode 100644 index 0000000..b667b19 --- /dev/null +++ b/packages/aws.iac/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "baseUrl": "." + } +} diff --git a/packages/aws.iac/tsup.config.ts b/packages/aws.iac/tsup.config.ts new file mode 100644 index 0000000..1544847 --- /dev/null +++ b/packages/aws.iac/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/resources.ts", "src/client.ts"], + dts: true, + format: ["esm"], + platform: "node", +}); diff --git a/packages/aws/package.json b/packages/aws/package.json index 235a289..344dd53 100644 --- a/packages/aws/package.json +++ b/packages/aws/package.json @@ -1,17 +1,11 @@ { + "type": "module", "name": "@notation/aws", "version": "0.0.1", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", "exports": { - "./api-gateway": { - "require": "./dist/api-gateway.js", - "import": "./dist/api-gateway.mjs" - }, - "./lambda": { - "require": "./dist/lambda.js", - "import": "./dist/lambda.mjs" + "./*": { + "import": "./dist/*/index.js", + "types": "./dist/*/index.d.ts" } }, "scripts": { @@ -19,7 +13,9 @@ "dev": "npm run build -- --watch" }, "dependencies": { + "@notation/aws.iac": "workspace:*", "@notation/core": "workspace:*", + "@notation/std.iac": "workspace:*", "@types/aws-lambda": "^8.10.125" } } diff --git a/packages/aws/src/api-gateway.ts b/packages/aws/src/api-gateway.ts deleted file mode 100644 index f861dd4..0000000 --- a/packages/aws/src/api-gateway.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { AwsResourceGroup } from "./core"; -import { ApiGatewayHandler, fn } from "./lambda"; - -export type ApiConfig = { - name: string; -}; - -export const api = (config: ApiConfig) => { - const apiGroup = new AwsResourceGroup({ type: "api", config: config }); - - const apiGateway = apiGroup.createResource({ - type: "api-gateway", - }); - - apiGroup.createResource({ - type: "api-gateway/stage", - dependencies: { - routerId: apiGateway.id, - }, - }); - - return apiGroup; -}; - -export const router = (apiGroup: ReturnType) => { - const createRouteCallback = - (method: string) => (path: string, handler: ApiGatewayHandler) => { - return route(apiGroup, method, path, handler); - }; - return { - get: createRouteCallback("GET"), - post: createRouteCallback("POST"), - put: createRouteCallback("PUT"), - patch: createRouteCallback("PATCH"), - delete: createRouteCallback("DELETE"), - }; -}; - -export const route = ( - apiGroup: ReturnType, - method: string, - path: string, - handler: ApiGatewayHandler, -) => { - const apiGateway = apiGroup.findResourceByType("api-gateway")!; - - // at compile time becomes infra module - const fnGroup = handler as any as ReturnType; - - const routeGroup = new AwsResourceGroup({ - type: "route", - dependencies: { - router: apiGroup.id, - fn: fnGroup.id, - }, - config: { - service: "aws/api-gateway", - path, - method, - }, - }); - - let integration; - - const lambda = fnGroup.findResourceByType("lambda")!; - const permission = fnGroup.findResourceByType("lambda/permission"); - integration = fnGroup.findResourceByType("lambda/integration"); - - if (!integration) { - integration = fnGroup.createResource({ - type: "lambda/integration", - dependencies: { - apiGatewayId: apiGateway.id, - lambdaId: lambda.id, - }, - }); - } - - if (!permission) { - fnGroup.createResource({ - type: "lambda/permission", - dependencies: { - apiGatewayId: apiGateway.id, - lambdaId: lambda.id, - }, - }); - } - - routeGroup.createResource({ - type: "api-gateway/route", - dependencies: { - apiGatewayId: apiGateway.id, - integrationId: integration.id, - }, - }); - - return routeGroup; -}; - -export const json = (result: any) => ({ - body: JSON.stringify(result), - statusCode: 200, -}); diff --git a/packages/aws/src/api-gateway/api.ts b/packages/aws/src/api-gateway/api.ts new file mode 100644 index 0000000..f6fe8ba --- /dev/null +++ b/packages/aws/src/api-gateway/api.ts @@ -0,0 +1,24 @@ +import { AwsResourceGroup } from "@notation/aws.iac/client"; +import * as aws from "@notation/aws.iac/resources"; + +export const api = (rgConfig: { name: string }) => { + const apiGroup = new AwsResourceGroup("api", rgConfig); + + const apiResource = apiGroup.add( + new aws.apiGateway.Api({ + config: { + Name: rgConfig.name, + ProtocolType: "HTTP", + }, + }), + ); + + apiGroup.add( + new aws.apiGateway.Stage({ + config: { StageName: "$default", AutoDeploy: true }, + dependencies: { router: apiResource }, + }), + ); + + return apiGroup; +}; diff --git a/packages/aws/src/api-gateway/index.ts b/packages/aws/src/api-gateway/index.ts new file mode 100644 index 0000000..ca8adcd --- /dev/null +++ b/packages/aws/src/api-gateway/index.ts @@ -0,0 +1,3 @@ +export * from "./api"; +export * from "./route"; +export * from "./router"; diff --git a/packages/aws/src/api-gateway/route.ts b/packages/aws/src/api-gateway/route.ts new file mode 100644 index 0000000..78307ab --- /dev/null +++ b/packages/aws/src/api-gateway/route.ts @@ -0,0 +1,67 @@ +import type { ApiGatewayHandler } from "src/shared"; +import * as aws from "@notation/aws.iac/resources"; +import { AwsResourceGroup } from "@notation/aws.iac/client"; +import { lambda } from "src/lambda"; +import { api } from "./api"; + +export const route = ( + apiGroup: ReturnType, + method: string, // todo: http methods only + path: `/${string}`, + handler: ApiGatewayHandler, +) => { + const apiResource = apiGroup.findResource(aws.apiGateway.Api)!; + + // at compile time becomes infra module + const lambdaGroup = handler as any as ReturnType; + + const routeGroup = new AwsResourceGroup("api/route", { + dependencies: { router: apiGroup.id, fn: lambdaGroup.id }, + }); + + let integration; + + const lambdaResource = lambdaGroup.findResource(aws.lambda.Lambda)!; + + const permission = lambdaGroup.findResource( + aws.lambda.LambdaApiGatewayPermission, + ); + + integration = lambdaGroup.findResource(aws.apiGateway.LambdaIntegration); + + if (!permission) { + lambdaGroup.add( + new aws.lambda.LambdaApiGatewayPermission({ + dependencies: { + api: apiResource, + lambda: lambdaResource, + }, + }), + ); + } + + if (!integration) { + integration = lambdaGroup.add( + new aws.apiGateway.LambdaIntegration({ + dependencies: { + api: apiResource, + lambda: lambdaResource, + }, + }), + ); + } + + routeGroup.add( + new aws.apiGateway.Route({ + config: { + RouteKey: `${method} ${path}`, + }, + dependencies: { + api: apiResource, + lambdaIntegration: integration, + }, + }), + ); + + return routeGroup; +}; diff --git a/packages/aws/src/api-gateway/router.ts b/packages/aws/src/api-gateway/router.ts new file mode 100644 index 0000000..0b84808 --- /dev/null +++ b/packages/aws/src/api-gateway/router.ts @@ -0,0 +1,17 @@ +import type { ApiGatewayHandler } from "src/shared"; +import { api, route } from "."; + +export const router = (apiGroup: ReturnType) => { + const createRouteCallback = + (method: string) => (path: `/${string}`, handler: ApiGatewayHandler) => { + return route(apiGroup, method, path, handler); + }; + + return { + get: createRouteCallback("GET"), + post: createRouteCallback("POST"), + put: createRouteCallback("PUT"), + patch: createRouteCallback("PATCH"), + delete: createRouteCallback("DELETE"), + }; +}; diff --git a/packages/aws/src/core.ts b/packages/aws/src/core.ts deleted file mode 100644 index f8b9397..0000000 --- a/packages/aws/src/core.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ResourceGroup, ResourceGroupOptions } from "@notation/core"; - -export class AwsResourceGroup extends ResourceGroup { - constructor(opts: ResourceGroupOptions) { - super(opts); - this.platform = "aws"; - } -} diff --git a/packages/aws/src/lambda.fn/api-responses.ts b/packages/aws/src/lambda.fn/api-responses.ts new file mode 100644 index 0000000..717678f --- /dev/null +++ b/packages/aws/src/lambda.fn/api-responses.ts @@ -0,0 +1,4 @@ +export const json = (result: any) => ({ + body: JSON.stringify(result), + statusCode: 200, +}); diff --git a/packages/aws/src/lambda.fn/config.ts b/packages/aws/src/lambda.fn/config.ts new file mode 100644 index 0000000..856989c --- /dev/null +++ b/packages/aws/src/lambda.fn/config.ts @@ -0,0 +1,5 @@ +export type LambdaConfig = { + service: "aws/lambda"; + memory?: number; + timeout?: number; +}; diff --git a/packages/aws/src/lambda.fn/handlers.ts b/packages/aws/src/lambda.fn/handlers.ts new file mode 100644 index 0000000..0992d3f --- /dev/null +++ b/packages/aws/src/lambda.fn/handlers.ts @@ -0,0 +1,8 @@ +import type { ApiGatewayHandler } from "src/shared/lambda.handler"; + +export const handle = { + apiRequest: + (handler: ApiGatewayHandler): ApiGatewayHandler => + async (...args) => + handler(...args), +}; diff --git a/packages/aws/src/lambda.fn/index.ts b/packages/aws/src/lambda.fn/index.ts new file mode 100644 index 0000000..e4c158c --- /dev/null +++ b/packages/aws/src/lambda.fn/index.ts @@ -0,0 +1,3 @@ +export * from "./api-responses"; +export * from "./config"; +export * from "./handlers"; diff --git a/packages/aws/src/lambda.ts b/packages/aws/src/lambda.ts deleted file mode 100644 index 09ae67b..0000000 --- a/packages/aws/src/lambda.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AwsResourceGroup } from "./core"; -import type { - Context, - APIGatewayProxyEvent, - APIGatewayProxyResultV2, -} from "aws-lambda"; - -export type FnConfig = { - service: "aws/lambda"; - memory?: number; - timeout?: number; -}; - -export type ApiGatewayHandler = ( - event: APIGatewayProxyEvent, - context: Context, -) => APIGatewayProxyResultV2 | Promise; - -export const handle = { - apiRequest: (handler: ApiGatewayHandler): ApiGatewayHandler => handler, -}; - -export const fn = (config: { handler: string }) => { - const functionGroup = new AwsResourceGroup({ type: "function", config }); - - const role = functionGroup.createResource({ - type: "iam/role", - }); - - const policyAttachment = functionGroup.createResource({ - type: "iam/policy-attachment", - dependencies: { - roleId: role.id, - }, - }); - - functionGroup.createResource({ - type: "lambda", - dependencies: { - policyId: policyAttachment.id, - }, - }); - - return functionGroup; -}; diff --git a/packages/aws/src/lambda/index.ts b/packages/aws/src/lambda/index.ts new file mode 100644 index 0000000..16044ca --- /dev/null +++ b/packages/aws/src/lambda/index.ts @@ -0,0 +1 @@ +export * from "./lambda"; diff --git a/packages/aws/src/lambda/lambda.ts b/packages/aws/src/lambda/lambda.ts new file mode 100644 index 0000000..0f59811 --- /dev/null +++ b/packages/aws/src/lambda/lambda.ts @@ -0,0 +1,53 @@ +import * as aws from "@notation/aws.iac/resources"; +import * as std from "@notation/std.iac"; +import { AwsResourceGroup } from "@notation/aws.iac/client"; + +export const lambda = (config: { fileName: string; handler: string }) => { + const functionGroup = new AwsResourceGroup("aws/function", { config }); + + const zipFile = functionGroup.add( + new std.Zip({ + config: { fileName: config.fileName }, + }), + ); + + const role = functionGroup.add( + new aws.lambda.LambdaIamRole({ + config: { RoleName: `${functionGroup.id}-role` }, + }), + ); + + const policyAttachment = functionGroup.add( + new aws.lambda.LambdaRolePolicyAttachment({ + config: { + // todo: move to resource, or provide default roles + PolicyArn: + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + dependencies: { role }, + }), + ); + + const lambdaResource = functionGroup.add( + new aws.lambda.Lambda({ + config: { + FunctionName: `function-${functionGroup.id}`, + Handler: `index.${config.handler}`, + Runtime: "nodejs18.x", + }, + dependencies: { + role, + policyAttachment, + zipFile, + }, + }), + ); + + functionGroup.add( + new aws.lambda.LambdaLogGroup({ + dependencies: { lambda: lambdaResource }, + }), + ); + + return functionGroup; +}; diff --git a/packages/aws/src/shared/index.ts b/packages/aws/src/shared/index.ts new file mode 100644 index 0000000..6afe224 --- /dev/null +++ b/packages/aws/src/shared/index.ts @@ -0,0 +1 @@ +export * from "./lambda.handler"; diff --git a/packages/aws/src/shared/lambda.handler.ts b/packages/aws/src/shared/lambda.handler.ts new file mode 100644 index 0000000..84ef3ff --- /dev/null +++ b/packages/aws/src/shared/lambda.handler.ts @@ -0,0 +1,10 @@ +import type { + Context, + APIGatewayProxyEvent, + APIGatewayProxyResultV2, +} from "aws-lambda"; + +export type ApiGatewayHandler = ( + event: APIGatewayProxyEvent, + context: Context, +) => APIGatewayProxyResultV2 | Promise; diff --git a/packages/aws/test/__snapshots__/api-gateway.test.ts.snap b/packages/aws/test/__snapshots__/api-gateway.test.ts.snap index 49af7a4..8fd723b 100644 --- a/packages/aws/test/__snapshots__/api-gateway.test.ts.snap +++ b/packages/aws/test/__snapshots__/api-gateway.test.ts.snap @@ -5,27 +5,49 @@ AwsResourceGroup { "config": { "name": "api", }, - "createResource": [Function], "dependencies": {}, - "findResourceByType": [Function], - "id": 1, + "id": 0, "platform": "aws", "resources": [ { - "config": {}, + "config": { + "Name": "api", + "ProtocolType": "HTTP", + }, "dependencies": {}, - "groupId": 1, - "id": 3, - "type": "api-gateway", + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway", }, { - "config": {}, + "config": { + "AutoDeploy": true, + "StageName": "$default", + }, "dependencies": { - "routerId": 3, + "router": { + "config": { + "Name": "api", + "ProtocolType": "HTTP", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway", + }, }, - "groupId": 1, - "id": 4, - "type": "api-gateway/stage", + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 1, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway/stage", }, ], "type": "api", @@ -34,270 +56,520 @@ AwsResourceGroup { exports[`route resource group snapshot 1`] = ` AwsResourceGroup { - "config": { - "method": "GET", - "path": "/hello", - "service": "aws/api-gateway", - }, - "createResource": [Function], + "config": {}, "dependencies": { - "fn": 3, - "router": 2, + "fn": 1, + "router": 0, }, - "findResourceByType": [Function], - "id": 4, + "id": 2, "platform": "aws", "resources": [ { - "config": {}, - "dependencies": { - "apiGatewayId": 5, - "integrationId": 10, + "config": { + "RouteKey": "GET /hello", }, - "groupId": 4, - "id": 12, - "type": "api-gateway/route", - }, - ], - "type": "route", -} -`; - -exports[`route resource group snapshot 2`] = ` -AwsResourceGroup { - "config": { - "handler": "handler.fn.js", - }, - "createResource": [Function], - "dependencies": {}, - "findResourceByType": [Function], - "id": 3, - "platform": "aws", - "resources": [ - { - "config": {}, - "dependencies": {}, - "groupId": 3, - "id": 7, - "type": "iam/role", - }, - { - "config": {}, - "dependencies": { - "roleId": 7, - }, - "groupId": 3, - "id": 8, - "type": "iam/policy-attachment", - }, - { - "config": {}, "dependencies": { - "policyId": 8, + "api": { + "config": { + "Name": "api", + "ProtocolType": "HTTP", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway", + }, + "lambdaIntegration": { + "config": {}, + "dependencies": { + "api": { + "config": { + "Name": "api", + "ProtocolType": "HTTP", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway", + }, + "lambda": { + "config": { + "FunctionName": "function-1", + "Handler": "index.handler.fn.js", + "Runtime": "nodejs18.x", + }, + "dependencies": { + "policyAttachment": { + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + "dependencies": { + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 4, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", + }, + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + "zipFile": { + "config": { + "fileName": "src/fns/handler.fn.js", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "std/zip", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 5, + "output": null, + "retryOn": [ + "InvalidParameterValueException", + ], + "type": "aws/lambda", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 8, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway/integration/lambda", + }, }, - "groupId": 3, + "deploy": [Function: AsyncFunction], + "groupId": 2, "id": 9, - "type": "lambda", - }, - { - "config": {}, - "dependencies": { - "apiGatewayId": 5, - "lambdaId": 9, - }, - "groupId": 3, - "id": 10, - "type": "lambda/integration", - }, - { - "config": {}, - "dependencies": { - "apiGatewayId": 5, - "lambdaId": 9, - }, - "groupId": 3, - "id": 11, - "type": "lambda/permission", + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway/route", }, ], - "type": "function", + "type": "api/route", } `; -exports[`route resource group idempotency snapshot 1`] = ` +exports[`route resource group snapshot 2`] = ` AwsResourceGroup { "config": { - "handler": "handler.fn.js", + "config": { + "fileName": "src/fns/handler.fn.js", + "handler": "handler.fn.js", + }, }, - "createResource": [Function], "dependencies": {}, - "findResourceByType": [Function], - "id": 6, + "id": 1, "platform": "aws", "resources": [ { - "config": {}, - "dependencies": {}, - "groupId": 6, - "id": 15, - "type": "iam/role", - }, - { - "config": {}, - "dependencies": { - "roleId": 15, + "config": { + "fileName": "src/fns/handler.fn.js", }, - "groupId": 6, - "id": 16, - "type": "iam/policy-attachment", - }, - { - "config": {}, - "dependencies": { - "policyId": 16, - }, - "groupId": 6, - "id": 17, - "type": "lambda", - }, - { - "config": {}, "dependencies": {}, - "groupId": 6, - "id": 18, - "type": "lambda/integration", + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "std/zip", }, { - "config": {}, + "config": { + "RoleName": "1-role", + }, "dependencies": {}, - "groupId": 6, - "id": 19, - "type": "lambda/permission", + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", }, - ], - "type": "function", -} -`; - -exports[`route resource group idempotency snapshot 2`] = ` -AwsResourceGroup { - "config": { - "method": "GET", - "path": "/hello", - "service": "aws/api-gateway", - }, - "createResource": [Function], - "dependencies": { - "fn": 6, - "router": 5, - }, - "findResourceByType": [Function], - "id": 7, - "platform": "aws", - "resources": [ { - "config": {}, - "dependencies": { - "apiGatewayId": 13, - "integrationId": 18, + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", }, - "groupId": 7, - "id": 20, - "type": "api-gateway/route", - }, - ], - "type": "route", -} -`; - -exports[`route resource group snapshot 3`] = ` -AwsResourceGroup { - "config": { - "method": "GET", - "path": "/hello", - "service": "aws/api-gateway", - }, - "createResource": [Function], - "dependencies": { - "fn": 6, - "router": 5, - }, - "findResourceByType": [Function], - "id": 7, - "platform": "aws", - "resources": [ - { - "config": {}, "dependencies": { - "apiGatewayId": 13, - "integrationId": 18, + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, }, - "groupId": 7, - "id": 20, - "type": "api-gateway/route", - }, - ], - "type": "route", -} -`; - -exports[`route resource group snapshot 4`] = ` -AwsResourceGroup { - "config": { - "handler": "handler.fn.js", - }, - "createResource": [Function], - "dependencies": {}, - "findResourceByType": [Function], - "id": 6, - "platform": "aws", - "resources": [ - { - "config": {}, - "dependencies": {}, - "groupId": 6, - "id": 15, - "type": "iam/role", + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 4, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", }, { - "config": {}, + "config": { + "FunctionName": "function-1", + "Handler": "index.handler.fn.js", + "Runtime": "nodejs18.x", + }, "dependencies": { - "roleId": 15, + "policyAttachment": { + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + "dependencies": { + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 4, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", + }, + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + "zipFile": { + "config": { + "fileName": "src/fns/handler.fn.js", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "std/zip", + }, }, - "groupId": 6, - "id": 16, - "type": "iam/policy-attachment", + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 5, + "output": null, + "retryOn": [ + "InvalidParameterValueException", + ], + "type": "aws/lambda", }, { "config": {}, "dependencies": { - "policyId": 16, + "lambda": { + "config": { + "FunctionName": "function-1", + "Handler": "index.handler.fn.js", + "Runtime": "nodejs18.x", + }, + "dependencies": { + "policyAttachment": { + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + "dependencies": { + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 4, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", + }, + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + "zipFile": { + "config": { + "fileName": "src/fns/handler.fn.js", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "std/zip", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 5, + "output": null, + "retryOn": [ + "InvalidParameterValueException", + ], + "type": "aws/lambda", + }, }, - "groupId": 6, - "id": 17, - "type": "lambda", + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 6, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/log-group", }, { "config": {}, "dependencies": { - "apiGatewayId": 13, - "lambdaId": 17, + "api": { + "config": { + "Name": "api", + "ProtocolType": "HTTP", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway", + }, + "lambda": { + "config": { + "FunctionName": "function-1", + "Handler": "index.handler.fn.js", + "Runtime": "nodejs18.x", + }, + "dependencies": { + "policyAttachment": { + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + "dependencies": { + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 4, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", + }, + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + "zipFile": { + "config": { + "fileName": "src/fns/handler.fn.js", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "std/zip", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 5, + "output": null, + "retryOn": [ + "InvalidParameterValueException", + ], + "type": "aws/lambda", + }, }, - "groupId": 6, - "id": 18, - "type": "lambda/integration", + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 7, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/permission/api-gateway", }, { "config": {}, "dependencies": { - "apiGatewayId": 13, - "lambdaId": 17, + "api": { + "config": { + "Name": "api", + "ProtocolType": "HTTP", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway", + }, + "lambda": { + "config": { + "FunctionName": "function-1", + "Handler": "index.handler.fn.js", + "Runtime": "nodejs18.x", + }, + "dependencies": { + "policyAttachment": { + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + "dependencies": { + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 4, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", + }, + "role": { + "config": { + "RoleName": "1-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 3, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + "zipFile": { + "config": { + "fileName": "src/fns/handler.fn.js", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "std/zip", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 5, + "output": null, + "retryOn": [ + "InvalidParameterValueException", + ], + "type": "aws/lambda", + }, }, - "groupId": 6, - "id": 19, - "type": "lambda/permission", + "deploy": [Function: AsyncFunction], + "groupId": 1, + "id": 8, + "output": null, + "retryOn": undefined, + "type": "aws/api-gateway/integration/lambda", }, ], - "type": "function", + "type": "aws/function", } `; diff --git a/packages/aws/test/__snapshots__/lambda.test.ts.snap b/packages/aws/test/__snapshots__/lambda.test.ts.snap index cbedd7a..0344adc 100644 --- a/packages/aws/test/__snapshots__/lambda.test.ts.snap +++ b/packages/aws/test/__snapshots__/lambda.test.ts.snap @@ -1,83 +1,210 @@ // Bun Snapshot v1, https://goo.gl/fbAQLP -exports[`fn 1`] = ` +exports[`lambda resource group snapshot 1`] = ` AwsResourceGroup { "config": { - "handler": "handler.fn.js", + "config": { + "fileName": "src/fns/handler.fn/index.js", + "handler": "handler", + }, }, - "createResource": [Function], "dependencies": {}, - "findResourceByType": [Function], "id": 0, "platform": "aws", "resources": [ { - "config": {}, + "config": { + "fileName": "src/fns/handler.fn/index.js", + }, "dependencies": {}, + "deploy": [Function: AsyncFunction], "groupId": 0, "id": 0, - "type": "iam/role", + "output": null, + "retryOn": undefined, + "type": "std/zip", }, { - "config": {}, - "dependencies": { - "roleId": 0, + "config": { + "RoleName": "0-role", }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], "groupId": 0, "id": 1, - "type": "iam/policy-attachment", + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", }, { - "config": {}, + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, "dependencies": { - "policyId": 1, + "role": { + "config": { + "RoleName": "0-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 1, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, }, + "deploy": [Function: AsyncFunction], "groupId": 0, "id": 2, - "type": "lambda", - }, - ], - "type": "function", -} -`; - -exports[`fn resource group snapshot 1`] = ` -AwsResourceGroup { - "config": { - "handler": "handler.fn.js", - }, - "createResource": [Function], - "dependencies": {}, - "findResourceByType": [Function], - "id": 0, - "platform": "aws", - "resources": [ - { - "config": {}, - "dependencies": {}, - "groupId": 0, - "id": 0, - "type": "iam/role", + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", }, { - "config": {}, + "config": { + "FunctionName": "function-0", + "Handler": "index.handler", + "Runtime": "nodejs18.x", + }, "dependencies": { - "roleId": 0, + "policyAttachment": { + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + "dependencies": { + "role": { + "config": { + "RoleName": "0-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 1, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", + }, + "role": { + "config": { + "RoleName": "0-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 1, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + "zipFile": { + "config": { + "fileName": "src/fns/handler.fn/index.js", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "std/zip", + }, }, + "deploy": [Function: AsyncFunction], "groupId": 0, - "id": 1, - "type": "iam/policy-attachment", + "id": 3, + "output": null, + "retryOn": [ + "InvalidParameterValueException", + ], + "type": "aws/lambda", }, { "config": {}, "dependencies": { - "policyId": 1, + "lambda": { + "config": { + "FunctionName": "function-0", + "Handler": "index.handler", + "Runtime": "nodejs18.x", + }, + "dependencies": { + "policyAttachment": { + "config": { + "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + }, + "dependencies": { + "role": { + "config": { + "RoleName": "0-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 1, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 2, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/policy-attachment", + }, + "role": { + "config": { + "RoleName": "0-role", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 1, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/role", + }, + "zipFile": { + "config": { + "fileName": "src/fns/handler.fn/index.js", + }, + "dependencies": {}, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 0, + "output": null, + "retryOn": undefined, + "type": "std/zip", + }, + }, + "deploy": [Function: AsyncFunction], + "groupId": 0, + "id": 3, + "output": null, + "retryOn": [ + "InvalidParameterValueException", + ], + "type": "aws/lambda", + }, }, + "deploy": [Function: AsyncFunction], "groupId": 0, - "id": 2, - "type": "lambda", + "id": 4, + "output": null, + "retryOn": undefined, + "type": "aws/lambda/log-group", }, ], - "type": "function", + "type": "aws/function", } `; diff --git a/packages/aws/test/api-gateway.test.ts b/packages/aws/test/api-gateway.test.ts index e128df9..5d063dd 100644 --- a/packages/aws/test/api-gateway.test.ts +++ b/packages/aws/test/api-gateway.test.ts @@ -1,12 +1,11 @@ -import { test, expect } from "bun:test"; -import { api, route, router, json } from "src/api-gateway"; -import { fn } from "src/lambda"; - -test("json returns a JSON string and a 200 status code", () => { - const payload = { message: "Hello, world!" }; - const response = json(payload); - expect(response.statusCode).toEqual(200); - expect(response.body).toEqual(JSON.stringify(payload)); +import { test, expect, beforeEach } from "bun:test"; +import { resetResourceGroupCounters } from "@notation/core"; +import { apiGateway } from "@notation/aws.iac/resources"; +import { api, route, router } from "src/api-gateway"; +import { lambda } from "src/lambda"; + +beforeEach(() => { + resetResourceGroupCounters(); }); test("api resource group snapshot", () => { @@ -16,7 +15,10 @@ test("api resource group snapshot", () => { test("route resource group snapshot", () => { const apiResourceGroup = api({ name: "api" }); - const fnResourceGroup = fn({ handler: "handler.fn.js" }); + const fnResourceGroup = lambda({ + fileName: "src/fns/handler.fn.js", + handler: "handler.fn.js", + }); const routeResourceGroup = route( apiResourceGroup, @@ -31,7 +33,10 @@ test("route resource group snapshot", () => { test("route resource group idempotency snapshot", () => { const apiResourceGroup = api({ name: "api" }); - const fnResourceGroup = fn({ handler: "handler.fn.js" }); + const fnResourceGroup = lambda({ + fileName: "src/fns/handler.fn.js", + handler: "handler.fn.js", + }); route(apiResourceGroup, "GET", "/hello", fnResourceGroup as any); const fnResourceGroupSnapshot = JSON.stringify(fnResourceGroup); @@ -44,11 +49,15 @@ test("route resource group idempotency snapshot", () => { test("router provides methods for each HTTP verb", () => { const apiResourceGroup = api({ name: "api" }); const apiRouter = router(apiResourceGroup); - const handler = fn({ handler: "handler.fn.js" }); + const handler = lambda({ + fileName: "src/fns/handler.fn.js", + handler: "handler.fn.js", + }); for (const method of ["GET", "POST", "PUT", "DELETE", "PATCH"]) { - const route = (apiRouter as any)[method.toLowerCase()]("/hello", handler); - expect(route.config.method).toEqual(method); - expect(route.config.path).toEqual("/hello"); + const routerKey = method.toLowerCase() as keyof typeof apiRouter; + const routeGroup = apiRouter[routerKey]("/hello", handler as any); + const route = routeGroup.findResource(apiGateway.Route)!; + expect(route.config.RouteKey).toEqual(`${method} /hello`); } }); diff --git a/packages/aws/test/lambda.fn.test.ts b/packages/aws/test/lambda.fn.test.ts new file mode 100644 index 0000000..07d1168 --- /dev/null +++ b/packages/aws/test/lambda.fn.test.ts @@ -0,0 +1,22 @@ +import { test, expect, beforeEach } from "bun:test"; +import { resetResourceGroupCounters } from "@notation/core"; +import { handle, json } from "src/lambda.fn"; + +beforeEach(() => { + resetResourceGroupCounters(); +}); + +test("handlers wrap user-provided handlers", async () => { + const fn = async () => ({ body: "{}" }); + for (const handler of Object.values(handle)) { + const result = await handler(fn)({} as any, {} as any); + expect(result).toEqual({ body: "{}" }); + } +}); + +test("json returns a JSON string and a 200 status code", () => { + const payload = { message: "Hello, world!" }; + const response = json(payload); + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual(JSON.stringify(payload)); +}); diff --git a/packages/aws/test/lambda.test.ts b/packages/aws/test/lambda.test.ts index 542973c..849f0d5 100644 --- a/packages/aws/test/lambda.test.ts +++ b/packages/aws/test/lambda.test.ts @@ -1,15 +1,15 @@ -import { test, expect } from "bun:test"; -import { handle, fn } from "src/lambda"; +import { beforeEach, test, expect } from "bun:test"; +import { lambda } from "src/lambda"; +import { resetResourceGroupCounters } from "@notation/core"; -test("handlers are identity functions", async () => { - const fn = () => ({}); - for (const handler of Object.values(handle)) { - const result = handler(fn); - expect(result).toEqual(fn); - } +beforeEach(() => { + resetResourceGroupCounters(); }); -test("fn resource group snapshot", async () => { - const fnResourceGroup = fn({ handler: "handler.fn.js" }); +test("lambda resource group snapshot", async () => { + const fnResourceGroup = lambda({ + fileName: "src/fns/handler.fn/index.js", + handler: "handler", + }); expect(fnResourceGroup).toMatchSnapshot(); }); diff --git a/packages/aws/tsup.config.ts b/packages/aws/tsup.config.ts index 0fb00cc..d5fc9be 100644 --- a/packages/aws/tsup.config.ts +++ b/packages/aws/tsup.config.ts @@ -1,9 +1,14 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/core.ts", "src/lambda.ts", "src/api-gateway.ts"], + entry: [ + "src/api-gateway/index.ts", + "src/lambda/index.ts", + "src/lambda.fn/index.ts", + "src/shared/index.ts", + ], splitting: false, dts: true, - clean: true, - format: ["cjs", "esm"], + format: ["esm"], + platform: "node", }); diff --git a/packages/cli/package.json b/packages/cli/package.json index 24a0fcc..5ee7da7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,14 +1,12 @@ { + "type": "module", "name": "@notation/cli", "version": "0.0.1", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", "bin": { "notation": "dist/index.js" }, "scripts": { - "build": "tsup src/index.ts --format cjs", + "build": "tsup", "dev": "npm run build -- --watch" }, "dependencies": { @@ -16,6 +14,7 @@ "@notation/esbuild-plugins": "workspace:*", "commander": "^11.0.0", "esbuild": "^0.19.3", - "glob": "^10.3.10" + "glob": "^10.3.10", + "fflate": "0.8.1" } } diff --git a/packages/cli/src/compile.ts b/packages/cli/src/compile.ts index 3b8c244..000c48c 100644 --- a/packages/cli/src/compile.ts +++ b/packages/cli/src/compile.ts @@ -1,25 +1,25 @@ +import fs from "fs"; +import * as fflate from "fflate"; +import { glob } from "glob"; +import { log } from "console"; import esbuild from "esbuild"; import { functionInfraPlugin, functionRuntimePlugin, } from "@notation/esbuild-plugins"; -import { - getResourceGroups, - getResources, - createMermaidFlowChart, - createMermaidLiveUrl, -} from "@notation/core"; -import { glob } from "glob"; -import path from "path"; +import { filePaths } from "@notation/core"; -export async function compile(infraEntryPoint: string) { - await compileInfra(infraEntryPoint); +export async function compile(entryPoint: string) { + await compileInfra(entryPoint); // @todo: fnEntryPoints could be an output of compileInfra const fnEntryPoints = await glob("**/*.fn.ts"); await compileFns(fnEntryPoints); + await zipFns(fnEntryPoints); } export async function compileInfra(entryPoint: string) { + log("Compiling infrastructure", entryPoint); + await esbuild.build({ entryPoints: [entryPoint], plugins: [functionInfraPlugin()], @@ -28,27 +28,39 @@ export async function compileInfra(entryPoint: string) { outExtension: { ".js": ".mjs" }, bundle: true, format: "esm", + platform: "node", treeShaking: true, - external: ["@notation/core"], + packages: "external", }); - const outFilePath = `dist/infra/${entryPoint.replace("ts", "mjs")}`; - await import(path.join(process.cwd(), outFilePath)); - const resourceGroups = getResourceGroups(); - const resources = getResources(); - const chart = createMermaidFlowChart(resourceGroups, resources); - const chartUrl = createMermaidLiveUrl(chart); - console.log("\nGenerated infrastructure chart:\n"); - console.log(chartUrl); } export async function compileFns(entryPoints: string[]) { - await esbuild.build({ - entryPoints: entryPoints, - plugins: [functionRuntimePlugin()], - outdir: "dist/runtime", - outbase: ".", - bundle: true, - format: "esm", - treeShaking: true, - }); + log("Compiling functions"); + + for (const entryPoint of entryPoints) { + await esbuild.build({ + entryPoints: [entryPoint], + plugins: [functionRuntimePlugin()], + outfile: filePaths.dist.runtime.index(entryPoint), + outExtension: { ".js": ".mjs" }, + bundle: true, + format: "esm", + platform: "node", + treeShaking: true, + }); + } +} + +export async function zipFns(entryPoints: string[]) { + log("Compiling deployable packages"); + + for (const entryPoint of entryPoints) { + const inputFilePath = filePaths.dist.runtime.index(entryPoint); + const inputFile = fs.readFileSync(inputFilePath); + + const zipFilePath = `${inputFilePath}.zip`; + const archive = fflate.zipSync({ "index.mjs": inputFile }, { level: 9 }); + + fs.writeFileSync(zipFilePath, archive); + } } diff --git a/packages/cli/src/deploy.ts b/packages/cli/src/deploy.ts new file mode 100644 index 0000000..d2a27b5 --- /dev/null +++ b/packages/cli/src/deploy.ts @@ -0,0 +1,16 @@ +import { compile } from "./compile"; +import { getResourceGraph } from "./utils"; + +export async function deploy(entryPoint: string) { + await compile(entryPoint); + await deployApp(entryPoint); +} + +export async function deployApp(entryPoint: string) { + console.log(`Deploying ${entryPoint}`); + const graph = await getResourceGraph(entryPoint); + for (const resource of graph.resources) { + await resource.runDeploy(); + console.log(`Deployed ${resource.type} ${resource.id}`); + } +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index a32e003..e4c2d08 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,6 +1,8 @@ #!/usr/bin/env node import { program } from "commander"; import { compile } from "./compile"; +import { deploy } from "./deploy"; +import { visualise } from "./visualise"; program .command("init") @@ -15,16 +17,23 @@ program .argument("", "entryPoint") .description("Compile Notation App") .action(async (entryPoint) => { - console.log("Compiling", entryPoint); await compile(entryPoint); }); +program + .command("viz") + .argument("", "entryPoint") + .description("Visualise Notation App") + .action(async (entryPoint) => { + await visualise(entryPoint); + }); + program .command("deploy") - .argument("", "environment") + .argument("", "entryPoint") .description("Deploy Notation App") - .action(async (stackName) => { - console.log(`Deploying stack ${stackName}...`); + .action(async (entryPoint) => { + await deploy(entryPoint); }); program.parse(process.argv); diff --git a/packages/cli/src/utils.ts b/packages/cli/src/utils.ts new file mode 100644 index 0000000..e2fe209 --- /dev/null +++ b/packages/cli/src/utils.ts @@ -0,0 +1,12 @@ +import path from "path"; +import { getResources, getResourceGroups, filePaths } from "@notation/core"; + +export async function getResourceGraph(entryPoint: string) { + const outFilePath = filePaths.dist.infra.index(entryPoint); + await import(path.join(process.cwd(), outFilePath)); + + const resourceGroups = getResourceGroups(); + const resources = getResources(); + + return { resourceGroups, resources }; +} diff --git a/packages/cli/src/visualise.ts b/packages/cli/src/visualise.ts new file mode 100644 index 0000000..7016478 --- /dev/null +++ b/packages/cli/src/visualise.ts @@ -0,0 +1,20 @@ +import { createMermaidFlowChart, createMermaidLiveUrl } from "@notation/core"; +import { getResourceGraph } from "./utils"; +import { log } from "console"; +import { compileInfra } from "./compile"; + +export async function visualise(entryPoint: string) { + await compileInfra(entryPoint); + await generateGraph(entryPoint); +} + +export async function generateGraph(entryPoint: string) { + log(`Generating graph for ${entryPoint}`); + + const graph = await getResourceGraph(entryPoint); + const chart = createMermaidFlowChart(graph.resourceGroups, graph.resources); + const chartUrl = createMermaidLiveUrl(chart); + + log("\nGenerated infrastructure chart:\n"); + log(chartUrl); +} diff --git a/packages/cli/tsup.config.ts b/packages/cli/tsup.config.ts new file mode 100644 index 0000000..187a26c --- /dev/null +++ b/packages/cli/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + platform: "node", +}); diff --git a/packages/core/package.json b/packages/core/package.json index 2604e9d..5079ee1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,20 +1,20 @@ { + "type": "module", "name": "@notation/core", "version": "0.0.1", "main": "./dist/index.js", - "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts", + "build": "tsup", "dev": "npm run build -- --watch" }, "dependencies": { - "@types/pako": "^2.0.2", "js-base64": "^3.7.5", "pako": "^2.1.0" }, "devDependencies": { "@types/common-tags": "^1.8.2", + "@types/pako": "^2.0.2", "common-tags": "^1.8.2" } } diff --git a/packages/core/src/chart.ts b/packages/core/src/chart.ts index c46d672..f710364 100644 --- a/packages/core/src/chart.ts +++ b/packages/core/src/chart.ts @@ -1,10 +1,11 @@ import pako from "pako"; import { fromUint8Array } from "js-base64"; -import { ResourceGroup, Resource } from "./resource-group"; +import { ResourceGroup } from "./resource-group"; +import { Resource } from "./resource"; export const createMermaidFlowChart = ( - resourceGroups: ResourceGroup<{}>[], - resources: Resource<{}>[], + resourceGroups: ResourceGroup[], + resources: Resource[], ): string => { let mermaidString = "flowchart TD\n"; let connectionsString = ""; @@ -17,10 +18,10 @@ export const createMermaidFlowChart = ( mermaidString += ` end\n`; group.resources.forEach((resource) => { - Object.values(resource.dependencies).forEach((depId) => { - const depResource = resources.find((r) => r.id === depId); + (Object.values(resource.dependencies) as Resource[]).forEach((dep) => { + const depResource = resources.find((r) => r.id === dep.id); if (depResource) { - connectionsString += ` ${resource.type}_${resource.id} --> ${depResource.type}_${depId}\n`; + connectionsString += ` ${resource.type}_${resource.id} --> ${depResource.type}_${dep.id}\n`; } }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 464907a..8d3b558 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,2 +1,6 @@ +export * from "./paths"; export * from "./chart"; +export * from "./resource"; export * from "./resource-group"; +export * from "./state-getters"; +export { resetResourceGroupCounters } from "./state"; diff --git a/packages/core/src/paths.ts b/packages/core/src/paths.ts new file mode 100644 index 0000000..847ccae --- /dev/null +++ b/packages/core/src/paths.ts @@ -0,0 +1,12 @@ +export const filePaths = { + dist: { + runtime: { + index: (entryPoint: string) => + `dist/runtime/${entryPoint.replace(/.ts$/, "/index.mjs")}`, + }, + infra: { + index: (entryPoint: string) => + `dist/infra/${entryPoint.replace(/.ts$/, ".mjs")}`, + }, + }, +}; diff --git a/packages/core/src/resource-group.ts b/packages/core/src/resource-group.ts index 1deff2e..60be8ea 100644 --- a/packages/core/src/resource-group.ts +++ b/packages/core/src/resource-group.ts @@ -1,75 +1,48 @@ -let resourceGroups: ResourceGroup<{}>[] = []; -let resources: Resource[] = []; - -let resourceGroupCounter = 0; -let resourceCounter = 0; - -export type Resource = { - id: number; - groupId: number; - type: string; - dependencies: Record; - config: Config; -}; - -export type ResourceOptions = { - type: string; +import { + resources, + resourceGroups, + getNextResourceCount, + getNextResourceGroupCount, +} from "./state"; +import { Resource } from "./resource"; + +export type ResourceGroupOptions = { dependencies?: Record; - config?: Config; + [key: string]: any; }; -export type ResourceGroupOptions = { +export abstract class ResourceGroup { type: string; - dependencies?: Record; - config?: Config; -}; - -export class ResourceGroup { id: number; - type: string; - platform: string; dependencies: Record; - config: Config; + config: Record; resources: Resource[]; - constructor(opts: ResourceGroupOptions) { - const { type, dependencies, config } = opts; - this.id = resourceGroupCounter++; + constructor(type: string, opts: ResourceGroupOptions) { + const { dependencies, ...config } = opts; this.type = type; - this.platform = "core"; + this.id = getNextResourceGroupCount(); this.dependencies = dependencies || {}; - this.config = config || ({} as Config); + this.config = config || {}; this.resources = []; resourceGroups.push(this); return this; } - createResource = ( - opts: ResourceOptions, - ) => { - const resource: Resource = { - id: resourceCounter++, - groupId: this.id, - type: opts.type, - dependencies: opts.dependencies || {}, - config: opts.config || ({} as ResourceConfig), - }; + add(resource: T) { + if (resources.includes(resource)) { + throw new Error(`Resource ${resource.type} has already been registered.`); + } + resource.id = getNextResourceCount(); + resource.groupId = this.id; resources.push(resource); this.resources.push(resource); return resource; - }; + } - findResourceByType = (type: string) => { - return this.resources.find((r) => r.type === type); - }; + findResource Resource>(ResourceClass: T) { + return this.resources.find((r) => r instanceof ResourceClass) as + | InstanceType + | undefined; + } } - -export const getResourceGroups = () => resourceGroups; -export const getResources = () => resources; - -export const reset = () => { - resources = []; - resourceGroups = []; - resourceCounter = 0; - resourceGroupCounter = 0; -}; diff --git a/packages/core/src/resource.ts b/packages/core/src/resource.ts new file mode 100644 index 0000000..a5a6e40 --- /dev/null +++ b/packages/core/src/resource.ts @@ -0,0 +1,93 @@ +import { OptionalIfAllPropertiesOptional } from "./types"; + +type ResourceOpts = OptionalIfAllPropertiesOptional<"config", C> & + OptionalIfAllPropertiesOptional<"dependencies", D>; + +export abstract class Resource< + Input = {}, + Output = {}, + Config = Input, + Dependencies extends Record = {}, +> { + abstract type: string; + abstract retryOn: string[] | undefined; + config: Config; + dependencies: Dependencies; + id: number = -1; + groupId: number = -1; + output: Output = null as Output; + + constructor(opts: ResourceOpts) { + this.config = opts.config || ({} as Config); + this.dependencies = opts.dependencies || ({} as Dependencies); + return this; + } + + abstract getDeployInput(): Promise | Input; + abstract deploy(input: Input): Promise; + + async runDeploy() { + let backoff = 1000; + try { + const input = await this.getDeployInput(); + this.output = await this.deploy(input); + } catch (err: any) { + if (this.retryOn?.includes(err.name)) { + console.log(`Retrying ${this.type} ${this.id}`); + await new Promise((resolve) => setTimeout(resolve, backoff)); + backoff *= 1.5; + await this.runDeploy(); + } else { + throw err; + } + } + } +} + +export function createResourceFactory< + Input = {}, + Output = {}, + Dependencies extends Record = {}, +>() { + type DerivedResourceConstructor = new ( + opts: ResourceOpts, + ) => Resource; + + function factory< + DefaultConfig extends Partial = Partial, + Config = Omit, + >(opts: { + type: string; + getIntrinsicConfig: ( + dependencies: Dependencies, + ) => Promise | DefaultConfig; + deploy: (input: Input) => Promise; + retryOn?: string[]; + }): DerivedResourceConstructor; + + function factory(opts: { + type: string; + deploy: (input: Input) => Promise; + }): DerivedResourceConstructor; + + function factory(opts: any) { + return class extends Resource { + type = opts.type; + retryOn = opts.retryOn; + + async getDeployInput() { + if ("getIntrinsicConfig" in opts) { + return { + ...this.config, + ...(await opts.getIntrinsicConfig(this.dependencies)), + } as Input; + } + return this.config as any as Input; + } + + deploy = opts.deploy; + }; + } + + return factory; +} diff --git a/packages/core/src/state-getters.ts b/packages/core/src/state-getters.ts new file mode 100644 index 0000000..dd9209a --- /dev/null +++ b/packages/core/src/state-getters.ts @@ -0,0 +1,4 @@ +import { resourceGroups, resources } from "./state"; + +export const getResourceGroups = () => resourceGroups; +export const getResources = () => resources; diff --git a/packages/core/src/state.ts b/packages/core/src/state.ts new file mode 100644 index 0000000..3b9bed5 --- /dev/null +++ b/packages/core/src/state.ts @@ -0,0 +1,23 @@ +import { Resource } from "./resource"; +import { ResourceGroup } from "./resource-group"; + +export let resourceGroups: ResourceGroup[] = []; +export let resources: Resource[] = []; + +let resourceGroupCounter = -1; +let resourceCounter = -1; + +export const getNextResourceCount = () => { + return ++resourceCounter; +}; + +export const getNextResourceGroupCount = () => { + return ++resourceGroupCounter; +}; + +export const resetResourceGroupCounters = () => { + resources = []; + resourceGroups = []; + resourceCounter = -1; + resourceGroupCounter = -1; +}; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..b5ef5a7 --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,15 @@ +export type IfAllPropertiesOptional = T extends Partial + ? Partial extends T + ? Y + : N + : N; + +/** + * @param K - The record key + * @param T - The record value type + * @description If T is Partial, return { [key in K]?: T }, else return { [key in K]: T } + */ +export type OptionalIfAllPropertiesOptional< + K extends string, + T, +> = IfAllPropertiesOptional; diff --git a/packages/core/test/chart.test.ts b/packages/core/test/chart.test.ts index 7614248..559f3ec 100644 --- a/packages/core/test/chart.test.ts +++ b/packages/core/test/chart.test.ts @@ -1,7 +1,8 @@ import { expect, it } from "bun:test"; import { stripIndent } from "common-tags"; import { createMermaidFlowChart, createMermaidLiveUrl } from "src/chart"; -import { ResourceGroup, Resource } from "src/resource-group"; +import { ResourceGroup } from "src/resource-group"; +import { Resource } from "src/resource"; it("should create a mermaid flowchart string", () => { const { resourceGroups, resources } = getFixture(); @@ -49,7 +50,15 @@ function getFixture() { groupId: 0, config: {}, type: "ResourceTypeB", - dependencies: { dep1: 0 }, + dependencies: { + dep1: { + id: 0, + groupId: 0, + config: {}, + type: "ResourceTypeA", + dependencies: {}, + }, + }, }, ], }, @@ -67,7 +76,7 @@ function getFixture() { }, ], }, - ] as ResourceGroup<{}>[]; + ] as ResourceGroup[]; const resources = [ { @@ -82,7 +91,15 @@ function getFixture() { groupId: 0, config: {}, type: "ResourceTypeB", - dependencies: { dep1: 0 }, + dependencies: { + dep1: { + id: 0, + groupId: 0, + config: {}, + type: "ResourceTypeA", + dependencies: {}, + }, + }, }, { id: 2, @@ -91,7 +108,7 @@ function getFixture() { type: "ResourceTypeC", dependencies: {}, }, - ] as Resource<{}>[]; + ] as Resource[]; return { resourceGroups, resources }; } diff --git a/packages/core/test/resource-group.test.ts b/packages/core/test/resource-group.test.ts new file mode 100644 index 0000000..0c7d529 --- /dev/null +++ b/packages/core/test/resource-group.test.ts @@ -0,0 +1,111 @@ +import { beforeEach, expect, it } from "bun:test"; +import { + createResourceFactory, + ResourceGroup, + getResourceGroups, + getResources, +} from "src"; +import { resetResourceGroupCounters } from "src/"; + +beforeEach(() => { + resetResourceGroupCounters(); +}); + +class TestResourceGroup extends ResourceGroup { + platform = "test-platform"; +} + +const TestResource = createResourceFactory<{ a: number }, {}>()({ + type: "test-resource", + deploy() { + throw new Error("Method not implemented."); + }, +}); + +const TestResource2 = createResourceFactory<{ a: number }, {}>()({ + type: "test-resource-2", + deploy() { + throw new Error("Method not implemented."); + }, +}); + +const testResource = new TestResource({ config: { a: 1 } }); +const testResource2 = new TestResource2({ config: { a: 2 } }); + +it("creates a resource group", () => { + const resourceGroup = new TestResourceGroup("test-group", { a: 1 }); + expect(resourceGroup.id).toBe(0); + expect(resourceGroup.type).toBe("test-group"); + expect(resourceGroup.platform).toBe("test-platform"); + expect(resourceGroup.config).toEqual({ a: 1 }); + expect(resourceGroup.resources).toEqual([]); +}); + +it("stores resource groups in the global array", () => { + new TestResourceGroup("test-group", { type: "test1" }); + new TestResourceGroup("test-group-2", { type: "test2" }); + + expect(getResourceGroups()).toHaveLength(2); + expect(getResourceGroups()[0].type).toBe("test-group"); + expect(getResourceGroups()[1].type).toBe("test-group-2"); +}); + +it("creates a resource within a group", () => { + const resourceGroup = new TestResourceGroup("test-group", { a: 1 }); + const resource = resourceGroup.add(testResource); + + expect(resource.id).toBe(0); + expect(resource.type).toBe("test-resource"); + expect(resource.groupId).toBe(resourceGroup.id); + expect(resourceGroup.resources).toContain(resource); +}); + +it("stores resources in the global array", () => { + const resourceGroup = new TestResourceGroup("test-group", { a: 1 }); + resourceGroup.add(testResource); + resourceGroup.add(testResource2); + + expect(getResources()).toHaveLength(2); + expect((getResources()[0].config as any).a).toBe(1); + expect((getResources()[1].config as any).a).toBe(2); +}); + +it("increments resource group IDs", () => { + const rg1 = new TestResourceGroup("test-group", { type: "group1" }); + const rg2 = new TestResourceGroup("test-group", { type: "group2" }); + + expect(rg1.id).toBe(0); + expect(rg2.id).toBe(1); +}); + +it("finds a resource within a group", () => { + const resourceGroup = new TestResourceGroup("test-group", { type: "group1" }); + const resource = resourceGroup.add(testResource); + + expect(resourceGroup.findResource(TestResource)).toBe(resource); + expect(resourceGroup.findResource(TestResource2)).toBe(undefined); +}); + +it("references resources within groups", () => { + const rg1 = new TestResourceGroup("test-group", {}); + const r1 = rg1.add(testResource); + const r2 = rg1.add(testResource2); + + expect(getResources()).toContain(r1); + expect(getResources()).toContain(r2); +}); + +it("throws an error when adding an existing resource", () => { + const rg1 = new TestResourceGroup("test-group", {}); + rg1.add(testResource); + expect(() => rg1.add(testResource)).toThrow(); +}); + +it("increments resource IDs globally", () => { + const rg1 = new TestResourceGroup("test-group", {}); + const r1 = rg1.add(testResource); + const rg2 = new TestResourceGroup("test-group", {}); + const r2 = rg2.add(testResource2); + expect(r1.id).toBe(0); + expect(r2.id).toBe(1); +}); diff --git a/packages/core/test/resource-group.ts b/packages/core/test/resource-group.ts deleted file mode 100644 index 5221bab..0000000 --- a/packages/core/test/resource-group.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { beforeEach, expect, it } from "bun:test"; -import { - ResourceGroup, - getResourceGroups, - getResources, - reset, -} from "src/resource-group"; - -beforeEach(() => { - reset(); -}); - -it("creates a resource group", () => { - const resourceGroup = new ResourceGroup({ type: "test" }); - expect(resourceGroup.id).toBe(0); - expect(resourceGroup.type).toBe("test"); - expect(resourceGroup.platform).toBe("core"); - expect(resourceGroup.resources).toEqual([]); -}); - -it("stores resource groups in the global array", () => { - new ResourceGroup({ type: "test" }); - new ResourceGroup({ type: "test2" }); - - expect(getResourceGroups()).toHaveLength(2); - expect(getResourceGroups()[0].type).toBe("test"); - expect(getResourceGroups()[1].type).toBe("test2"); -}); - -it("creates a resource within a group", () => { - const resourceGroup = new ResourceGroup({ type: "testGroup" }); - const resource = resourceGroup.createResource({ type: "testResource" }); - - expect(resource.id).toBe(0); - expect(resource.type).toBe("testResource"); - expect(resource.groupId).toBe(resourceGroup.id); - expect(resourceGroup.resources).toContain(resource); -}); - -it("stores resources in the global array", () => { - const resourceGroup = new ResourceGroup({ type: "testGroup" }); - resourceGroup.createResource({ type: "testResource1" }); - resourceGroup.createResource({ type: "testResource2" }); - - expect(getResources()).toHaveLength(2); - expect(getResources()[0].type).toBe("testResource1"); - expect(getResources()[1].type).toBe("testResource2"); -}); - -it("finds a resource by type within a group", () => { - const resourceGroup = new ResourceGroup({ type: "testGroup" }); - const resource = resourceGroup.createResource({ type: "testResource" }); - - expect(resourceGroup.findResourceByType("testResource")).toBe(resource); - expect(resourceGroup.findResourceByType("nonexistentType")).toBeUndefined(); -}); - -it("increments resource group IDs", () => { - const rg1 = new ResourceGroup({ type: "group1" }); - const rg2 = new ResourceGroup({ type: "group2" }); - - expect(rg1.id).toBe(0); - expect(rg2.id).toBe(1); -}); - -it("increments resource IDs globally", () => { - const rg1 = new ResourceGroup({ type: "group1" }); - const r1 = rg1.createResource({ type: "resource1" }); - const rg2 = new ResourceGroup({ type: "group2" }); - const r2 = rg2.createResource({ type: "resource2" }); - const r3 = rg1.createResource({ type: "resource3" }); - - expect(r1.id).toBe(0); - expect(r2.id).toBe(1); - expect(r3.id).toBe(2); -}); - -it("references resources within groups", () => { - const rg1 = new ResourceGroup({ type: "group1" }); - const r1 = rg1.createResource({ type: "resource1" }); - const r2 = rg1.createResource({ type: "resource2" }); - - expect(getResources()).toContain(r1); - expect(getResources()).toContain(r2); -}); diff --git a/packages/core/test/resource.test.ts b/packages/core/test/resource.test.ts new file mode 100644 index 0000000..b9b0061 --- /dev/null +++ b/packages/core/test/resource.test.ts @@ -0,0 +1,107 @@ +import { expect, it, mock, test } from "bun:test"; +import { Resource, createResourceFactory } from "src"; + +it("creates a resource class factory", () => { + class TestResource extends Resource<{ name: string }, { id: number }> { + type = "test"; + retryOn = []; + getDeployInput() { + return { name: "testName" }; + } + deploy() { + return Promise.resolve({ id: 123 }); + } + } + + const resource = new TestResource({ + config: { name: "sampleName" }, + }); + + expect(resource.config.name).toBe("sampleName"); + expect(resource.type).toBe("test"); +}); + +test("merges config and intrinsic config", async () => { + const factory = createResourceFactory<{ name: string }>(); + + const TestResource = factory({ + type: "testTypeWithOverride", + getIntrinsicConfig: () => ({ name: "intrinsicName" }), + deploy: () => Promise.resolve({}), + }); + + const resource = new TestResource({ config: { name: "overrideName" } }); + expect((await resource.getDeployInput()).name).toBe("intrinsicName"); +}); + +it("passes dependencies to getIntrinsicConfig", () => { + const getIntrinsicConfigMock = mock((deps) => deps.dep1); + + const childFactory = createResourceFactory< + { name: string }, + { id: number } + >(); + + const ChildResource = childFactory({ + type: "childType", + deploy: () => Promise.resolve({ id: 123 }), + }); + + const childResource = new ChildResource({ config: { name: "child-name" } }); + + const factory = createResourceFactory< + { name: string }, + { id: number }, + { dep1: Resource } + >(); + + const Resource = factory({ + type: "testTypeWithDeps", + deploy: () => Promise.resolve({ id: 123 }), + getIntrinsicConfig: getIntrinsicConfigMock, + }); + + const resource = new Resource({ + dependencies: { + dep1: childResource, + }, + }); + + resource.getDeployInput(); + + expect(getIntrinsicConfigMock.mock.calls[0]).toEqual([ + { dep1: childResource }, + ]); +}); + +it('passes merged config to "deploy"', async () => { + const deployMock = mock(() => Promise.resolve({})); + + const factory = createResourceFactory<{ name: string; a: 1 }>(); + + const TestResource = factory({ + type: "testType", + deploy: deployMock, + getIntrinsicConfig: () => ({ a: 1 }), + }); + + const resource = new TestResource({ config: { name: "testName" } }); + + await resource.runDeploy(); + expect(deployMock.mock.calls[0]).toEqual([{ a: 1, name: "testName" }]); +}); + +it("assigns outputs after deploy", async () => { + const factory = createResourceFactory<{ name: string }, { id: number }>(); + + const TestResource = factory({ + type: "testType", + deploy: () => Promise.resolve({ id: 123 }), + }); + + const resource = new TestResource({ config: { name: "testName" } }); + expect(resource.output).toBe(null); + + await resource.runDeploy(); + expect(resource.output).toEqual({ id: 123 }); +}); diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts new file mode 100644 index 0000000..f0ac238 --- /dev/null +++ b/packages/core/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + dts: true, + format: ["esm"], +}); diff --git a/packages/esbuild-plugins/package.json b/packages/esbuild-plugins/package.json index a005361..41ba48a 100644 --- a/packages/esbuild-plugins/package.json +++ b/packages/esbuild-plugins/package.json @@ -1,14 +1,15 @@ { + "type": "module", "name": "@notation/esbuild-plugins", "version": "0.0.1", "main": "./dist/index.js", - "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "scripts": { - "build": "tsup src/index.ts --format cjs,esm --dts", + "build": "tsup", "dev": "npm run build -- --watch" }, "dependencies": { + "@notation/core": "workspace:*", "esbuild": "^0.19.3", "typescript": "^5.2.2" }, diff --git a/packages/esbuild-plugins/src/parsers/parse-fn-module.ts b/packages/esbuild-plugins/src/parsers/parse-fn-module.ts index 0b1d478..2d46908 100644 --- a/packages/esbuild-plugins/src/parsers/parse-fn-module.ts +++ b/packages/esbuild-plugins/src/parsers/parse-fn-module.ts @@ -1,4 +1,4 @@ -import * as ts from "typescript"; +import ts from "typescript"; export function parseFnModule(input: string) { const sourceFile = ts.createSourceFile( diff --git a/packages/esbuild-plugins/src/parsers/remove-config-export.ts b/packages/esbuild-plugins/src/parsers/remove-config-export.ts index 1fbed45..33f22a2 100644 --- a/packages/esbuild-plugins/src/parsers/remove-config-export.ts +++ b/packages/esbuild-plugins/src/parsers/remove-config-export.ts @@ -1,4 +1,4 @@ -import * as ts from "typescript"; +import ts from "typescript"; export function removeConfigExport(sourceText: string): string { const sourceFile = ts.createSourceFile( diff --git a/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts b/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts index c41f842..72beca8 100644 --- a/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts +++ b/packages/esbuild-plugins/src/plugins/function-infra-plugin.ts @@ -2,6 +2,7 @@ import path from "node:path"; import { Plugin } from "esbuild"; import { parseFnModule } from "src/parsers/parse-fn-module"; import { GetFile, fsGetFile, withFileCheck } from "src/utils/get-file"; +import { filePaths } from "@notation/core"; type PluginOpts = { getFile?: GetFile; @@ -15,17 +16,19 @@ export function functionInfraPlugin(opts: PluginOpts = {}): Plugin { const getFile = withFileCheck(opts.getFile || fsGetFile); const fileContent = await getFile(args.path); const fileName = path.relative(process.cwd(), args.path); + const outFileName = filePaths.dist.runtime.index(fileName); const { config, configRaw, exports } = parseFnModule(fileContent); const reservedNames = ["preload", "config"]; + const [platform, service] = (config.service as string).split("/"); - let infraCode = `import { fn } from "@notation/${config.service}"`; + let infraCode = `import { ${service} } from "@notation/${platform}/${service}";`; infraCode = infraCode.concat(`\nconst config = ${configRaw};`); for (const handlerName of exports) { if (reservedNames.includes(handlerName)) continue; infraCode = infraCode.concat( - `\nexport const ${handlerName} = fn({ fileName: "${fileName}", handler: "${handlerName}", ...config });`, + `\nexport const ${handlerName} = ${service}({ fileName: "${outFileName}", handler: "${handlerName}", ...config });`, ); } diff --git a/packages/esbuild-plugins/test/plugins/function-infra-plugin.test.ts b/packages/esbuild-plugins/test/plugins/function-infra-plugin.test.ts index 3777fa2..7035b30 100644 --- a/packages/esbuild-plugins/test/plugins/function-infra-plugin.test.ts +++ b/packages/esbuild-plugins/test/plugins/function-infra-plugin.test.ts @@ -16,9 +16,9 @@ it("remaps exports", async () => { `; const expected = stripIndent` - import { fn } from "@notation/aws/lambda"; + import { lambda } from "@notation/aws/lambda"; const config = { service: "aws/lambda" }; - export const getNum = fn({ fileName: "entry.fn.ts", handler: "getNum", ...config }); + export const getNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getNum", ...config }); `; const output = await buildInfra(input); @@ -28,16 +28,16 @@ it("remaps exports", async () => { it("merges config", async () => { const input = ` - import { FnConfig } from "@notation/aws/lambda"; + import { LambdaConfig } from "@notation/aws/lambda"; import { handler } from "@notation/aws/api-gateway"; export const getNum = handler(() => 1); - export const config: FnConfig = { service: "aws/lambda", memory: 64 }; + export const config: LambdaConfig = { service: "aws/lambda", memory: 64 }; `; const expected = stripIndent` - import { fn } from "@notation/aws/lambda"; + import { lambda } from "@notation/aws/lambda"; const config = { service: "aws/lambda", memory: 64 }; - export const getNum = fn({ fileName: "entry.fn.ts", handler: "getNum", ...config }); + export const getNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getNum", ...config }); `; const output = await buildInfra(input); @@ -47,7 +47,7 @@ it("merges config", async () => { it("should strip runtime code", async () => { const input = ` - import { FnConfig } from "@notation/aws/lambda"; + import { LambdaConfig } from "@notation/aws/lambda"; import { handler } from "@notation/aws/api-gateway"; import lib from "lib"; @@ -58,10 +58,10 @@ it("should strip runtime code", async () => { export const config = { service: "aws/lambda" };`; const expected = stripIndent` - import { fn } from "@notation/aws/lambda"; + import { lambda } from "@notation/aws/lambda"; const config = { service: "aws/lambda" }; - export const getNum = fn({ fileName: "entry.fn.ts", handler: "getNum", ...config }); - export const getDoubleNum = fn({ fileName: "entry.fn.ts", handler: "getDoubleNum", ...config }); + export const getNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getNum", ...config }); + export const getDoubleNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getDoubleNum", ...config }); `; const output = await buildInfra(input); diff --git a/packages/esbuild-plugins/test/plugins/function-runtime-plugin.test.ts b/packages/esbuild-plugins/test/plugins/function-runtime-plugin.test.ts index e813d1e..7db629f 100644 --- a/packages/esbuild-plugins/test/plugins/function-runtime-plugin.test.ts +++ b/packages/esbuild-plugins/test/plugins/function-runtime-plugin.test.ts @@ -10,7 +10,7 @@ const buildRuntime = createBuilder((input) => ({ it("strips infra code", async () => { const input = stripIndent` - import { FnConfig } from "@notation/aws/lambda"; + import { LambdaConfig } from "@notation/aws/lambda"; import { handler } from "@notation/aws/api-gateway"; const num = await fetch("http://api.com/num").then((res) => res.json()); @@ -18,7 +18,7 @@ it("strips infra code", async () => { export const getNum = handler(() => num); export const getDoubleNum = handler(() => num * 2); - export const config: FnConfig = { + export const config: LambdaConfig = { memory: 64, timeout: 5, environment: "node:16", diff --git a/packages/esbuild-plugins/tsup.config.ts b/packages/esbuild-plugins/tsup.config.ts new file mode 100644 index 0000000..f0ac238 --- /dev/null +++ b/packages/esbuild-plugins/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + dts: true, + format: ["esm"], +}); diff --git a/packages/std.iac/package.json b/packages/std.iac/package.json new file mode 100644 index 0000000..56df6ef --- /dev/null +++ b/packages/std.iac/package.json @@ -0,0 +1,14 @@ +{ + "type": "module", + "name": "@notation/std.iac", + "version": "0.0.1", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsup", + "dev": "npm run build -- --watch" + }, + "dependencies": { + "@notation/core": "workspace:*" + } +} diff --git a/packages/std.iac/src/index.ts b/packages/std.iac/src/index.ts new file mode 100644 index 0000000..f51c5b4 --- /dev/null +++ b/packages/std.iac/src/index.ts @@ -0,0 +1,20 @@ +import { createResourceFactory } from "@notation/core"; +import path from "node:path"; +import fs from "node:fs/promises"; + +export type ZipInput = { fileName: string }; +export type ZipOutput = { contents: Buffer }; + +const createZipClass = createResourceFactory(); + +export const Zip = createZipClass({ + type: "std/zip", + + deploy: async (config: ZipInput) => { + const zipPath = path.join(process.cwd(), `${config.fileName}.zip`); + const zipFile = await fs.readFile(zipPath); + return { contents: zipFile }; + }, +}); + +export type ZipFileInstance = InstanceType; diff --git a/packages/std.iac/tsconfig.json b/packages/std.iac/tsconfig.json new file mode 100644 index 0000000..b667b19 --- /dev/null +++ b/packages/std.iac/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "tsconfig/base.json", + "compilerOptions": { + "baseUrl": "." + } +} diff --git a/packages/std.iac/tsup.config.ts b/packages/std.iac/tsup.config.ts new file mode 100644 index 0000000..3abd9e5 --- /dev/null +++ b/packages/std.iac/tsup.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + dts: true, + format: ["esm"], + platform: "node", +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fdfe5ab..4eaa368 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,42 @@ importers: packages/aws: dependencies: + '@notation/aws.iac': + specifier: workspace:* + version: link:../aws.iac '@notation/core': specifier: workspace:* version: link:../core + '@notation/std.iac': + specifier: workspace:* + version: link:../std.iac + '@types/aws-lambda': + specifier: ^8.10.125 + version: 8.10.125 + + packages/aws.iac: + dependencies: + '@aws-sdk/client-apigatewayv2': + specifier: ^3.441.0 + version: 3.454.0 + '@aws-sdk/client-cloudwatch-logs': + specifier: ^3.441.0 + version: 3.454.0 + '@aws-sdk/client-iam': + specifier: ^3.441.0 + version: 3.454.0 + '@aws-sdk/client-lambda': + specifier: ^3.441.0 + version: 3.454.0 + '@aws-sdk/client-sts': + specifier: ^3.441.0 + version: 3.454.0 + '@notation/core': + specifier: workspace:* + version: link:../core + '@notation/std.iac': + specifier: workspace:* + version: link:../std.iac '@types/aws-lambda': specifier: ^8.10.125 version: 8.10.125 @@ -65,15 +98,15 @@ importers: esbuild: specifier: ^0.19.3 version: 0.19.3 + fflate: + specifier: 0.8.1 + version: 0.8.1 glob: specifier: ^10.3.10 version: 10.3.10 packages/core: dependencies: - '@types/pako': - specifier: ^2.0.2 - version: 2.0.2 js-base64: specifier: ^3.7.5 version: 3.7.5 @@ -84,12 +117,18 @@ importers: '@types/common-tags': specifier: ^1.8.2 version: 1.8.2 + '@types/pako': + specifier: ^2.0.2 + version: 2.0.2 common-tags: specifier: ^1.8.2 version: 1.8.2 packages/esbuild-plugins: dependencies: + '@notation/core': + specifier: workspace:* + version: link:../core esbuild: specifier: ^0.19.3 version: 0.19.3 @@ -104,6 +143,12 @@ importers: specifier: ^1.8.2 version: 1.8.2 + packages/std.iac: + dependencies: + '@notation/core': + specifier: workspace:* + version: link:../core + packages/tsconfig: {} test/compiler.test.app: @@ -124,6 +169,608 @@ importers: packages: + /@aws-crypto/crc32@3.0.0: + resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.451.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/ie11-detection@3.0.0: + resolution: {integrity: sha512-341lBBkiY1DfDNKai/wXM3aujNBkXR7tq1URPQDL9wi3AUbI80NR74uF1TXHMm7po1AcnFk8iu2S2IeU/+/A+Q==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-browser@3.0.0: + resolution: {integrity: sha512-8VLmW2B+gjFbU5uMeqtQM6Nj0/F1bro80xQXCW6CQBWgosFWXTx77aeOF5CAIAmbOK64SdMBJdNr6J41yP5mvQ==} + dependencies: + '@aws-crypto/ie11-detection': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-crypto/supports-web-crypto': 3.0.0 + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-locate-window': 3.310.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/sha256-js@3.0.0: + resolution: {integrity: sha512-PnNN7os0+yd1XvXAy23CFOmTbMaDxgxXtTKHybrJ39Y8kGzBATgBFibWJKH6BhytLI/Zyszs87xCOBNyBig6vQ==} + dependencies: + '@aws-crypto/util': 3.0.0 + '@aws-sdk/types': 3.451.0 + tslib: 1.14.1 + dev: false + + /@aws-crypto/supports-web-crypto@3.0.0: + resolution: {integrity: sha512-06hBdMwUAb2WFTuGG73LSC0wfPu93xWwo5vL2et9eymgmu3Id5vFAHBbajVWiGhPO37qcsdCap/FqXvJGJWPIg==} + dependencies: + tslib: 1.14.1 + dev: false + + /@aws-crypto/util@3.0.0: + resolution: {integrity: sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w==} + dependencies: + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-utf8-browser': 3.259.0 + tslib: 1.14.1 + dev: false + + /@aws-sdk/client-apigatewayv2@3.454.0: + resolution: {integrity: sha512-TRohVG0lCFZBkUiLhBoKU+8U3/jt3hxusNvU5uuTxyaVj0/MWau4h9kHnvSjXlxpdtOvwqN65uBLApJw/tkZHA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.454.0 + '@aws-sdk/core': 3.451.0 + '@aws-sdk/credential-provider-node': 3.451.0 + '@aws-sdk/middleware-host-header': 3.451.0 + '@aws-sdk/middleware-logger': 3.451.0 + '@aws-sdk/middleware-recursion-detection': 3.451.0 + '@aws-sdk/middleware-signing': 3.451.0 + '@aws-sdk/middleware-user-agent': 3.451.0 + '@aws-sdk/region-config-resolver': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@aws-sdk/util-user-agent-browser': 3.451.0 + '@aws-sdk/util-user-agent-node': 3.451.0 + '@smithy/config-resolver': 2.0.19 + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/hash-node': 2.0.16 + '@smithy/invalid-dependency': 2.0.14 + '@smithy/middleware-content-length': 2.0.16 + '@smithy/middleware-endpoint': 2.2.1 + '@smithy/middleware-retry': 2.0.21 + '@smithy/middleware-serde': 2.0.14 + '@smithy/middleware-stack': 2.0.8 + '@smithy/node-config-provider': 2.1.6 + '@smithy/node-http-handler': 2.1.10 + '@smithy/protocol-http': 3.0.10 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.20 + '@smithy/util-defaults-mode-node': 2.0.26 + '@smithy/util-endpoints': 1.0.5 + '@smithy/util-retry': 2.0.7 + '@smithy/util-stream': 2.0.21 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-cloudwatch-logs@3.454.0: + resolution: {integrity: sha512-anXMEIZvDvqsFAURYmNHaJU8SH85Rqkahkk0TsDiTLc6/J4Qh8xvcem358qTiXzRpPJmZe4m20XKqL0fXsJgIw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.454.0 + '@aws-sdk/core': 3.451.0 + '@aws-sdk/credential-provider-node': 3.451.0 + '@aws-sdk/middleware-host-header': 3.451.0 + '@aws-sdk/middleware-logger': 3.451.0 + '@aws-sdk/middleware-recursion-detection': 3.451.0 + '@aws-sdk/middleware-signing': 3.451.0 + '@aws-sdk/middleware-user-agent': 3.451.0 + '@aws-sdk/region-config-resolver': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@aws-sdk/util-user-agent-browser': 3.451.0 + '@aws-sdk/util-user-agent-node': 3.451.0 + '@smithy/config-resolver': 2.0.19 + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/hash-node': 2.0.16 + '@smithy/invalid-dependency': 2.0.14 + '@smithy/middleware-content-length': 2.0.16 + '@smithy/middleware-endpoint': 2.2.1 + '@smithy/middleware-retry': 2.0.21 + '@smithy/middleware-serde': 2.0.14 + '@smithy/middleware-stack': 2.0.8 + '@smithy/node-config-provider': 2.1.6 + '@smithy/node-http-handler': 2.1.10 + '@smithy/protocol-http': 3.0.10 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.20 + '@smithy/util-defaults-mode-node': 2.0.26 + '@smithy/util-endpoints': 1.0.5 + '@smithy/util-retry': 2.0.7 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + uuid: 8.3.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-iam@3.454.0: + resolution: {integrity: sha512-feBcpbCyBfeLBC2L+1BQnAZ0KhPGdlxAzX4HLk3v6dSoVGFPyS9/7NTISiXy5iVc7fdFBtosiqvDBMpajrXpAA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.454.0 + '@aws-sdk/core': 3.451.0 + '@aws-sdk/credential-provider-node': 3.451.0 + '@aws-sdk/middleware-host-header': 3.451.0 + '@aws-sdk/middleware-logger': 3.451.0 + '@aws-sdk/middleware-recursion-detection': 3.451.0 + '@aws-sdk/middleware-signing': 3.451.0 + '@aws-sdk/middleware-user-agent': 3.451.0 + '@aws-sdk/region-config-resolver': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@aws-sdk/util-user-agent-browser': 3.451.0 + '@aws-sdk/util-user-agent-node': 3.451.0 + '@smithy/config-resolver': 2.0.19 + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/hash-node': 2.0.16 + '@smithy/invalid-dependency': 2.0.14 + '@smithy/middleware-content-length': 2.0.16 + '@smithy/middleware-endpoint': 2.2.1 + '@smithy/middleware-retry': 2.0.21 + '@smithy/middleware-serde': 2.0.14 + '@smithy/middleware-stack': 2.0.8 + '@smithy/node-config-provider': 2.1.6 + '@smithy/node-http-handler': 2.1.10 + '@smithy/protocol-http': 3.0.10 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.20 + '@smithy/util-defaults-mode-node': 2.0.26 + '@smithy/util-endpoints': 1.0.5 + '@smithy/util-retry': 2.0.7 + '@smithy/util-utf8': 2.0.2 + '@smithy/util-waiter': 2.0.14 + fast-xml-parser: 4.2.5 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-lambda@3.454.0: + resolution: {integrity: sha512-nYak+ojl0H0AG0WTF2894npak4Uj2slBr09+3lBUz4rwPol93TsUHy8/5GfGLcqPMNnEKOknc4jioJOK7cb2Pw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/client-sts': 3.454.0 + '@aws-sdk/core': 3.451.0 + '@aws-sdk/credential-provider-node': 3.451.0 + '@aws-sdk/middleware-host-header': 3.451.0 + '@aws-sdk/middleware-logger': 3.451.0 + '@aws-sdk/middleware-recursion-detection': 3.451.0 + '@aws-sdk/middleware-signing': 3.451.0 + '@aws-sdk/middleware-user-agent': 3.451.0 + '@aws-sdk/region-config-resolver': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@aws-sdk/util-user-agent-browser': 3.451.0 + '@aws-sdk/util-user-agent-node': 3.451.0 + '@smithy/config-resolver': 2.0.19 + '@smithy/eventstream-serde-browser': 2.0.14 + '@smithy/eventstream-serde-config-resolver': 2.0.14 + '@smithy/eventstream-serde-node': 2.0.14 + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/hash-node': 2.0.16 + '@smithy/invalid-dependency': 2.0.14 + '@smithy/middleware-content-length': 2.0.16 + '@smithy/middleware-endpoint': 2.2.1 + '@smithy/middleware-retry': 2.0.21 + '@smithy/middleware-serde': 2.0.14 + '@smithy/middleware-stack': 2.0.8 + '@smithy/node-config-provider': 2.1.6 + '@smithy/node-http-handler': 2.1.10 + '@smithy/protocol-http': 3.0.10 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.20 + '@smithy/util-defaults-mode-node': 2.0.26 + '@smithy/util-endpoints': 1.0.5 + '@smithy/util-retry': 2.0.7 + '@smithy/util-stream': 2.0.21 + '@smithy/util-utf8': 2.0.2 + '@smithy/util-waiter': 2.0.14 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sso@3.451.0: + resolution: {integrity: sha512-KkYSke3Pdv3MfVH/5fT528+MKjMyPKlcLcd4zQb0x6/7Bl7EHrPh1JZYjzPLHelb+UY5X0qN8+cb8iSu1eiwIQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/core': 3.451.0 + '@aws-sdk/middleware-host-header': 3.451.0 + '@aws-sdk/middleware-logger': 3.451.0 + '@aws-sdk/middleware-recursion-detection': 3.451.0 + '@aws-sdk/middleware-user-agent': 3.451.0 + '@aws-sdk/region-config-resolver': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@aws-sdk/util-user-agent-browser': 3.451.0 + '@aws-sdk/util-user-agent-node': 3.451.0 + '@smithy/config-resolver': 2.0.19 + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/hash-node': 2.0.16 + '@smithy/invalid-dependency': 2.0.14 + '@smithy/middleware-content-length': 2.0.16 + '@smithy/middleware-endpoint': 2.2.1 + '@smithy/middleware-retry': 2.0.21 + '@smithy/middleware-serde': 2.0.14 + '@smithy/middleware-stack': 2.0.8 + '@smithy/node-config-provider': 2.1.6 + '@smithy/node-http-handler': 2.1.10 + '@smithy/protocol-http': 3.0.10 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.20 + '@smithy/util-defaults-mode-node': 2.0.26 + '@smithy/util-endpoints': 1.0.5 + '@smithy/util-retry': 2.0.7 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/client-sts@3.454.0: + resolution: {integrity: sha512-0fDvr8WeB6IYO8BUCzcivWmahgGl/zDbaYfakzGnt4mrl5ztYaXE875WI6b7+oFcKMRvN+KLvwu5TtyFuNY+GQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/core': 3.451.0 + '@aws-sdk/credential-provider-node': 3.451.0 + '@aws-sdk/middleware-host-header': 3.451.0 + '@aws-sdk/middleware-logger': 3.451.0 + '@aws-sdk/middleware-recursion-detection': 3.451.0 + '@aws-sdk/middleware-sdk-sts': 3.451.0 + '@aws-sdk/middleware-signing': 3.451.0 + '@aws-sdk/middleware-user-agent': 3.451.0 + '@aws-sdk/region-config-resolver': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@aws-sdk/util-user-agent-browser': 3.451.0 + '@aws-sdk/util-user-agent-node': 3.451.0 + '@smithy/config-resolver': 2.0.19 + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/hash-node': 2.0.16 + '@smithy/invalid-dependency': 2.0.14 + '@smithy/middleware-content-length': 2.0.16 + '@smithy/middleware-endpoint': 2.2.1 + '@smithy/middleware-retry': 2.0.21 + '@smithy/middleware-serde': 2.0.14 + '@smithy/middleware-stack': 2.0.8 + '@smithy/node-config-provider': 2.1.6 + '@smithy/node-http-handler': 2.1.10 + '@smithy/protocol-http': 3.0.10 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.20 + '@smithy/util-defaults-mode-node': 2.0.26 + '@smithy/util-endpoints': 1.0.5 + '@smithy/util-retry': 2.0.7 + '@smithy/util-utf8': 2.0.2 + fast-xml-parser: 4.2.5 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/core@3.451.0: + resolution: {integrity: sha512-SamWW2zHEf1ZKe3j1w0Piauryl8BQIlej0TBS18A4ACzhjhWXhCs13bO1S88LvPR5mBFXok3XOT6zPOnKDFktw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/smithy-client': 2.1.16 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-env@3.451.0: + resolution: {integrity: sha512-9dAav7DcRgaF7xCJEQR5ER9ErXxnu/tdnVJ+UPmb1NPeIZdESv1A3lxFDEq1Fs8c4/lzAj9BpshGyJVIZwZDKg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/property-provider': 2.0.15 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-ini@3.451.0: + resolution: {integrity: sha512-TySt64Ci5/ZbqFw1F9Z0FIGvYx5JSC9e6gqDnizIYd8eMnn8wFRUscRrD7pIHKfrhvVKN5h0GdYovmMO/FMCBw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.451.0 + '@aws-sdk/credential-provider-process': 3.451.0 + '@aws-sdk/credential-provider-sso': 3.451.0 + '@aws-sdk/credential-provider-web-identity': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@smithy/credential-provider-imds': 2.1.2 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-node@3.451.0: + resolution: {integrity: sha512-AEwM1WPyxUdKrKyUsKyFqqRFGU70e4qlDyrtBxJnSU9NRLZI8tfEZ67bN7fHSxBUBODgDXpMSlSvJiBLh5/3pw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/credential-provider-env': 3.451.0 + '@aws-sdk/credential-provider-ini': 3.451.0 + '@aws-sdk/credential-provider-process': 3.451.0 + '@aws-sdk/credential-provider-sso': 3.451.0 + '@aws-sdk/credential-provider-web-identity': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@smithy/credential-provider-imds': 2.1.2 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-process@3.451.0: + resolution: {integrity: sha512-HQywSdKeD5PErcLLnZfSyCJO+6T+ZyzF+Lm/QgscSC+CbSUSIPi//s15qhBRVely/3KBV6AywxwNH+5eYgt4lQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/credential-provider-sso@3.451.0: + resolution: {integrity: sha512-Usm/N51+unOt8ID4HnQzxIjUJDrkAQ1vyTOC0gSEEJ7h64NSSPGD5yhN7il5WcErtRd3EEtT1a8/GTC5TdBctg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/client-sso': 3.451.0 + '@aws-sdk/token-providers': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/credential-provider-web-identity@3.451.0: + resolution: {integrity: sha512-Xtg3Qw65EfDjWNG7o2xD6sEmumPfsy3WDGjk2phEzVg8s7hcZGxf5wYwe6UY7RJvlEKrU0rFA+AMn6Hfj5oOzg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/property-provider': 2.0.15 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-host-header@3.451.0: + resolution: {integrity: sha512-j8a5jAfhWmsK99i2k8oR8zzQgXrsJtgrLxc3js6U+525mcZytoiDndkWTmD5fjJ1byU1U2E5TaPq+QJeDip05Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/protocol-http': 3.0.10 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-logger@3.451.0: + resolution: {integrity: sha512-0kHrYEyVeB2QBfP6TfbI240aRtatLZtcErJbhpiNUb+CQPgEL3crIjgVE8yYiJumZ7f0jyjo8HLPkwD1/2APaw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-recursion-detection@3.451.0: + resolution: {integrity: sha512-J6jL6gJ7orjHGM70KDRcCP7so/J2SnkN4vZ9YRLTeeZY6zvBuHDjX8GCIgSqPn/nXFXckZO8XSnA7u6+3TAT0w==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/protocol-http': 3.0.10 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-sdk-sts@3.451.0: + resolution: {integrity: sha512-UJ6UfVUEgp0KIztxpAeelPXI5MLj9wUtUCqYeIMP7C1ZhoEMNm3G39VLkGN43dNhBf1LqjsV9jkKMZbVfYXuwg==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/middleware-signing': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-signing@3.451.0: + resolution: {integrity: sha512-s5ZlcIoLNg1Huj4Qp06iKniE8nJt/Pj1B/fjhWc6cCPCM7XJYUCejCnRh6C5ZJoBEYodjuwZBejPc1Wh3j+znA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/property-provider': 2.0.15 + '@smithy/protocol-http': 3.0.10 + '@smithy/signature-v4': 2.0.16 + '@smithy/types': 2.6.0 + '@smithy/util-middleware': 2.0.7 + tslib: 2.6.2 + dev: false + + /@aws-sdk/middleware-user-agent@3.451.0: + resolution: {integrity: sha512-8NM/0JiKLNvT9wtAQVl1DFW0cEO7OvZyLSUBLNLTHqyvOZxKaZ8YFk7d8PL6l76LeUKRxq4NMxfZQlUIRe0eSA==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@smithy/protocol-http': 3.0.10 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/region-config-resolver@3.451.0: + resolution: {integrity: sha512-3iMf4OwzrFb4tAAmoROXaiORUk2FvSejnHIw/XHvf/jjR4EqGGF95NZP/n/MeFZMizJWVssrwS412GmoEyoqhg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.6 + '@smithy/types': 2.6.0 + '@smithy/util-config-provider': 2.0.0 + '@smithy/util-middleware': 2.0.7 + tslib: 2.6.2 + dev: false + + /@aws-sdk/token-providers@3.451.0: + resolution: {integrity: sha512-ij1L5iUbn6CwxVOT1PG4NFjsrsKN9c4N1YEM0lkl6DwmaNOscjLKGSNyj9M118vSWsOs1ZDbTwtj++h0O/BWrQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-crypto/sha256-browser': 3.0.0 + '@aws-crypto/sha256-js': 3.0.0 + '@aws-sdk/middleware-host-header': 3.451.0 + '@aws-sdk/middleware-logger': 3.451.0 + '@aws-sdk/middleware-recursion-detection': 3.451.0 + '@aws-sdk/middleware-user-agent': 3.451.0 + '@aws-sdk/region-config-resolver': 3.451.0 + '@aws-sdk/types': 3.451.0 + '@aws-sdk/util-endpoints': 3.451.0 + '@aws-sdk/util-user-agent-browser': 3.451.0 + '@aws-sdk/util-user-agent-node': 3.451.0 + '@smithy/config-resolver': 2.0.19 + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/hash-node': 2.0.16 + '@smithy/invalid-dependency': 2.0.14 + '@smithy/middleware-content-length': 2.0.16 + '@smithy/middleware-endpoint': 2.2.1 + '@smithy/middleware-retry': 2.0.21 + '@smithy/middleware-serde': 2.0.14 + '@smithy/middleware-stack': 2.0.8 + '@smithy/node-config-provider': 2.1.6 + '@smithy/node-http-handler': 2.1.10 + '@smithy/property-provider': 2.0.15 + '@smithy/protocol-http': 3.0.10 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-base64': 2.0.1 + '@smithy/util-body-length-browser': 2.0.0 + '@smithy/util-body-length-node': 2.1.0 + '@smithy/util-defaults-mode-browser': 2.0.20 + '@smithy/util-defaults-mode-node': 2.0.26 + '@smithy/util-endpoints': 1.0.5 + '@smithy/util-retry': 2.0.7 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + transitivePeerDependencies: + - aws-crt + dev: false + + /@aws-sdk/types@3.451.0: + resolution: {integrity: sha512-rhK+qeYwCIs+laJfWCcrYEjay2FR/9VABZJ2NRM89jV/fKqGVQR52E5DQqrI+oEIL5JHMhhnr4N4fyECMS35lw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-endpoints@3.451.0: + resolution: {integrity: sha512-giqLGBTnRIcKkDqwU7+GQhKbtJ5Ku35cjGQIfMyOga6pwTBUbaK0xW1Sdd8sBQ1GhApscnChzI9o/R9x0368vw==} + engines: {node: '>=14.0.0'} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/util-endpoints': 1.0.5 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-locate-window@3.310.0: + resolution: {integrity: sha512-qo2t/vBTnoXpjKxlsC2e1gBrRm80M3bId27r0BRB2VniSSe7bL1mmzM+/HFtujm0iAxtPM+aLEflLJlJeDPg0w==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-browser@3.451.0: + resolution: {integrity: sha512-Ws5mG3J0TQifH7OTcMrCTexo7HeSAc3cBgjfhS/ofzPUzVCtsyg0G7I6T7wl7vJJETix2Kst2cpOsxygPgPD9w==} + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/types': 2.6.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-user-agent-node@3.451.0: + resolution: {integrity: sha512-TBzm6P+ql4mkGFAjPlO1CI+w3yUT+NulaiALjl/jNX/nnUp6HsJsVxJf4nVFQTG5KRV0iqMypcs7I3KIhH+LmA==} + engines: {node: '>=14.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + dependencies: + '@aws-sdk/types': 3.451.0 + '@smithy/node-config-provider': 2.1.6 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@aws-sdk/util-utf8-browser@3.259.0: + resolution: {integrity: sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==} + dependencies: + tslib: 2.6.2 + dev: false + /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} @@ -823,6 +1470,405 @@ packages: dev: false optional: true + /@smithy/abort-controller@2.0.14: + resolution: {integrity: sha512-zXtteuYLWbSXnzI3O6xq3FYvigYZFW8mdytGibfarLL2lxHto9L3ILtGVnVGmFZa7SDh62l39EnU5hesLN87Fw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/config-resolver@2.0.19: + resolution: {integrity: sha512-JsghnQ5zjWmjEVY8TFOulLdEOCj09SjRLugrHlkPZTIBBm7PQitCFVLThbsKPZQOP7N3ME1DU1nKUc1UaVnBog==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.6 + '@smithy/types': 2.6.0 + '@smithy/util-config-provider': 2.0.0 + '@smithy/util-middleware': 2.0.7 + tslib: 2.6.2 + dev: false + + /@smithy/credential-provider-imds@2.1.2: + resolution: {integrity: sha512-Y62jBWdoLPSYjr9fFvJf+KwTa1EunjVr6NryTEWCnwIY93OJxwV4t0qxjwdPl/XMsUkq79ppNJSEQN6Ohnhxjw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.6 + '@smithy/property-provider': 2.0.15 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-codec@2.0.14: + resolution: {integrity: sha512-g/OU/MeWGfHDygoXgMWfG/Xb0QqDnAGcM9t2FRrVAhleXYRddGOEnfanR5cmHgB9ue52MJsyorqFjckzXsylaA==} + dependencies: + '@aws-crypto/crc32': 3.0.0 + '@smithy/types': 2.6.0 + '@smithy/util-hex-encoding': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-browser@2.0.14: + resolution: {integrity: sha512-41wmYE9smDGJi1ZXp+LogH6BR7MkSsQD91wneIFISF/mupKULvoOJUkv/Nf0NMRxWlM3Bf1Vvi9FlR2oV4KU8Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.14 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-config-resolver@2.0.14: + resolution: {integrity: sha512-43IyRIzQ82s+5X+t/3Ood00CcWtAXQdmUIUKMed2Qg9REPk8SVIHhpm3rwewLwg+3G2Nh8NOxXlEQu6DsPUcMw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-node@2.0.14: + resolution: {integrity: sha512-jVh9E2qAr6DxH5tWfCAl9HV6tI0pEQ3JVmu85JknDvYTC66djcjDdhctPV2EHuKWf2kjRiFJcMIn0eercW4THA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-serde-universal': 2.0.14 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/eventstream-serde-universal@2.0.14: + resolution: {integrity: sha512-Ie35+AISNn1NmEjn5b2SchIE49pvKp4Q74bE9ME5RULWI1MgXyGkQUajWd5E6OBSr/sqGcs+rD3IjPErXnCm9g==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-codec': 2.0.14 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/fetch-http-handler@2.2.7: + resolution: {integrity: sha512-iSDBjxuH9TgrtMYAr7j5evjvkvgwLY3y+9D547uep+JNkZ1ZT+BaeU20j6I/bO/i26ilCWFImrlXTPsfQtZdIQ==} + dependencies: + '@smithy/protocol-http': 3.0.10 + '@smithy/querystring-builder': 2.0.14 + '@smithy/types': 2.6.0 + '@smithy/util-base64': 2.0.1 + tslib: 2.6.2 + dev: false + + /@smithy/hash-node@2.0.16: + resolution: {integrity: sha512-Wbi9A0PacMYUOwjAulQP90Wl3mQ6NDwnyrZQzFjDz+UzjXOSyQMgBrTkUBz+pVoYVlX3DUu24gWMZBcit+wOGg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + dev: false + + /@smithy/invalid-dependency@2.0.14: + resolution: {integrity: sha512-d8ohpwZo9RzTpGlAfsWtfm1SHBSU7+N4iuZ6MzR10xDTujJJWtmXYHK1uzcr7rggbpUTaWyHpPFgnf91q0EFqQ==} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/is-array-buffer@2.0.0: + resolution: {integrity: sha512-z3PjFjMyZNI98JFRJi/U0nGoLWMSJlDjAW4QUX2WNZLas5C0CmVV6LJ01JI0k90l7FvpmixjWxPFmENSClQ7ug==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/middleware-content-length@2.0.16: + resolution: {integrity: sha512-9ddDia3pp1d3XzLXKcm7QebGxLq9iwKf+J1LapvlSOhpF8EM9SjMeSrMOOFgG+2TfW5K3+qz4IAJYYm7INYCng==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/protocol-http': 3.0.10 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/middleware-endpoint@2.2.1: + resolution: {integrity: sha512-dVDS7HNJl/wb0lpByXor6whqDbb1YlLoaoWYoelyYzLHioXOE7y/0iDwJWtDcN36/tVCw9EPBFZ3aans84jLpg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-serde': 2.0.14 + '@smithy/node-config-provider': 2.1.6 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.6.0 + '@smithy/url-parser': 2.0.14 + '@smithy/util-middleware': 2.0.7 + tslib: 2.6.2 + dev: false + + /@smithy/middleware-retry@2.0.21: + resolution: {integrity: sha512-EZS1EXv1k6IJX6hyu/0yNQuPcPaXwG8SWljQHYueyRbOxmqYgoWMWPtfZj0xRRQ4YtLawQSpBgAeiJltq8/MPw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.6 + '@smithy/protocol-http': 3.0.10 + '@smithy/service-error-classification': 2.0.7 + '@smithy/types': 2.6.0 + '@smithy/util-middleware': 2.0.7 + '@smithy/util-retry': 2.0.7 + tslib: 2.6.2 + uuid: 8.3.2 + dev: false + + /@smithy/middleware-serde@2.0.14: + resolution: {integrity: sha512-hFi3FqoYWDntCYA2IGY6gJ6FKjq2gye+1tfxF2HnIJB5uW8y2DhpRNBSUMoqP+qvYzRqZ6ntv4kgbG+o3pX57g==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/middleware-stack@2.0.8: + resolution: {integrity: sha512-7/N59j0zWqVEKExJcA14MrLDZ/IeN+d6nbkN8ucs+eURyaDUXWYlZrQmMOd/TyptcQv0+RDlgag/zSTTV62y/Q==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/node-config-provider@2.1.6: + resolution: {integrity: sha512-HLqTs6O78m3M3z1cPLFxddxhEPv5MkVatfPuxoVO3A+cHZanNd/H5I6btcdHy6N2CB1MJ/lihJC92h30SESsBA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/property-provider': 2.0.15 + '@smithy/shared-ini-file-loader': 2.2.5 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/node-http-handler@2.1.10: + resolution: {integrity: sha512-lkALAwtN6odygIM4nB8aHDahINM6WXXjNrZmWQAh0RSossySRT2qa31cFv0ZBuAYVWeprskRk13AFvvLmf1WLw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.14 + '@smithy/protocol-http': 3.0.10 + '@smithy/querystring-builder': 2.0.14 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/property-provider@2.0.15: + resolution: {integrity: sha512-YbRFBn8oiiC3o1Kn3a4KjGa6k47rCM9++5W9cWqYn9WnkyH+hBWgfJAckuxpyA2Hq6Ys4eFrWzXq6fqHEw7iew==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/protocol-http@3.0.10: + resolution: {integrity: sha512-6+tjNk7rXW7YTeGo9qwxXj/2BFpJTe37kTj3EnZCoX/nH+NP/WLA7O83fz8XhkGqsaAhLUPo/bB12vvd47nsmg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/querystring-builder@2.0.14: + resolution: {integrity: sha512-lQ4pm9vTv9nIhl5jt6uVMPludr6syE2FyJmHsIJJuOD7QPIJnrf9HhUGf1iHh9KJ4CUv21tpOU3X6s0rB6uJ0g==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + '@smithy/util-uri-escape': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/querystring-parser@2.0.14: + resolution: {integrity: sha512-+cbtXWI9tNtQjlgQg3CA+pvL3zKTAxPnG3Pj6MP89CR3vi3QMmD0SOWoq84tqZDnJCxlsusbgIXk1ngMReXo+A==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/service-error-classification@2.0.7: + resolution: {integrity: sha512-LLxgW12qGz8doYto15kZ4x1rHjtXl0BnCG6T6Wb8z2DI4PT9cJfOSvzbuLzy7+5I24PAepKgFeWHRd9GYy3Z9w==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + dev: false + + /@smithy/shared-ini-file-loader@2.2.5: + resolution: {integrity: sha512-LHA68Iu7SmNwfAVe8egmjDCy648/7iJR/fK1UnVw+iAOUJoEYhX2DLgVd5pWllqdDiRbQQzgaHLcRokM+UFR1w==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/signature-v4@2.0.16: + resolution: {integrity: sha512-ilLY85xS2kZZzTb83diQKYLIYALvart0KnBaKnIRnMBHAGEio5aHSlANQoxVn0VsonwmQ3CnWhnCT0sERD8uTg==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/eventstream-codec': 2.0.14 + '@smithy/is-array-buffer': 2.0.0 + '@smithy/types': 2.6.0 + '@smithy/util-hex-encoding': 2.0.0 + '@smithy/util-middleware': 2.0.7 + '@smithy/util-uri-escape': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + dev: false + + /@smithy/smithy-client@2.1.16: + resolution: {integrity: sha512-Lw67+yQSpLl4YkDLUzI2KgS8TXclXmbzSeOJUmRFS4ueT56B4pw3RZRF/SRzvgyxM/HxgkUan8oSHXCujPDafQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/middleware-stack': 2.0.8 + '@smithy/types': 2.6.0 + '@smithy/util-stream': 2.0.21 + tslib: 2.6.2 + dev: false + + /@smithy/types@2.6.0: + resolution: {integrity: sha512-PgqxJq2IcdMF9iAasxcqZqqoOXBHufEfmbEUdN1pmJrJltT42b0Sc8UiYSWWzKkciIp9/mZDpzYi4qYG1qqg6g==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/url-parser@2.0.14: + resolution: {integrity: sha512-kbu17Y1AFXi5lNlySdDj7ZzmvupyWKCX/0jNZ8ffquRyGdbDZb+eBh0QnWqsSmnZa/ctyWaTf7n4l/pXLExrnw==} + dependencies: + '@smithy/querystring-parser': 2.0.14 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-base64@2.0.1: + resolution: {integrity: sha512-DlI6XFYDMsIVN+GH9JtcRp3j02JEVuWIn/QOZisVzpIAprdsxGveFed0bjbMRCqmIFe8uetn5rxzNrBtIGrPIQ==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/util-buffer-from': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-body-length-browser@2.0.0: + resolution: {integrity: sha512-JdDuS4ircJt+FDnaQj88TzZY3+njZ6O+D3uakS32f2VNnDo3vyEuNdBOh/oFd8Df1zSZOuH1HEChk2AOYDezZg==} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-body-length-node@2.1.0: + resolution: {integrity: sha512-/li0/kj/y3fQ3vyzn36NTLGmUwAICb7Jbe/CsWCktW363gh1MOcpEcSO3mJ344Gv2dqz8YJCLQpb6hju/0qOWw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-buffer-from@2.0.0: + resolution: {integrity: sha512-/YNnLoHsR+4W4Vf2wL5lGv0ksg8Bmk3GEGxn2vEQt52AQaPSCuaO5PM5VM7lP1K9qHRKHwrPGktqVoAHKWHxzw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/is-array-buffer': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-config-provider@2.0.0: + resolution: {integrity: sha512-xCQ6UapcIWKxXHEU4Mcs2s7LcFQRiU3XEluM2WcCjjBtQkUN71Tb+ydGmJFPxMUrW/GWMgQEEGipLym4XG0jZg==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-defaults-mode-browser@2.0.20: + resolution: {integrity: sha512-QJtnbTIl0/BbEASkx1MUFf6EaoWqWW1/IM90N++8NNscePvPf77GheYfpoPis6CBQawUWq8QepTP2QUSAdrVkw==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/property-provider': 2.0.15 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + bowser: 2.11.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-defaults-mode-node@2.0.26: + resolution: {integrity: sha512-lGFPOFCHv1ql019oegYqa54BZH7HREw6EBqjDLbAr0wquMX0BDi2sg8TJ6Eq+JGLijkZbJB73m4+aK8OFAapMg==} + engines: {node: '>= 10.0.0'} + dependencies: + '@smithy/config-resolver': 2.0.19 + '@smithy/credential-provider-imds': 2.1.2 + '@smithy/node-config-provider': 2.1.6 + '@smithy/property-provider': 2.0.15 + '@smithy/smithy-client': 2.1.16 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-endpoints@1.0.5: + resolution: {integrity: sha512-K7qNuCOD5K/90MjHvHm9kJldrfm40UxWYQxNEShMFxV/lCCCRIg8R4uu1PFAxRvPxNpIdcrh1uK6I1ISjDXZJw==} + engines: {node: '>= 14.0.0'} + dependencies: + '@smithy/node-config-provider': 2.1.6 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-hex-encoding@2.0.0: + resolution: {integrity: sha512-c5xY+NUnFqG6d7HFh1IFfrm3mGl29lC+vF+geHv4ToiuJCBmIfzx6IeHLg+OgRdPFKDXIw6pvi+p3CsscaMcMA==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-middleware@2.0.7: + resolution: {integrity: sha512-tRINOTlf1G9B0ECarFQAtTgMhpnrMPSa+5j4ZEwEawCLfTFTavk6757sxhE4RY5RMlD/I3x+DCS8ZUiR8ho9Pw==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-retry@2.0.7: + resolution: {integrity: sha512-fIe5yARaF0+xVT1XKcrdnHKTJ1Vc4+3e3tLDjCuIcE9b6fkBzzGFY7AFiX4M+vj6yM98DrwkuZeHf7/hmtVp0Q==} + engines: {node: '>= 14.0.0'} + dependencies: + '@smithy/service-error-classification': 2.0.7 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-stream@2.0.21: + resolution: {integrity: sha512-0BUE16d7n1x7pi1YluXJdB33jOTyBChT0j/BlOkFa9uxfg6YqXieHxjHNuCdJRARa7AZEj32LLLEPJ1fSa4inA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/fetch-http-handler': 2.2.7 + '@smithy/node-http-handler': 2.1.10 + '@smithy/types': 2.6.0 + '@smithy/util-base64': 2.0.1 + '@smithy/util-buffer-from': 2.0.0 + '@smithy/util-hex-encoding': 2.0.0 + '@smithy/util-utf8': 2.0.2 + tslib: 2.6.2 + dev: false + + /@smithy/util-uri-escape@2.0.0: + resolution: {integrity: sha512-ebkxsqinSdEooQduuk9CbKcI+wheijxEb3utGXkCoYQkJnwTnLbH1JXGimJtUkQwNQbsbuYwG2+aFVyZf5TLaw==} + engines: {node: '>=14.0.0'} + dependencies: + tslib: 2.6.2 + dev: false + + /@smithy/util-utf8@2.0.2: + resolution: {integrity: sha512-qOiVORSPm6Ce4/Yu6hbSgNHABLP2VMv8QOC3tTDNHHlWY19pPyc++fBTbZPtx6egPXi4HQxKDnMxVxpbtX2GoA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/util-buffer-from': 2.0.0 + tslib: 2.6.2 + dev: false + + /@smithy/util-waiter@2.0.14: + resolution: {integrity: sha512-Q6gSz4GUNjNGhrfNg+2Mjy+7K4pEI3r82x1b/+3dSc03MQqobMiUrRVN/YK/4nHVagvBELCoXsiHAFQJNQ5BeA==} + engines: {node: '>=14.0.0'} + dependencies: + '@smithy/abort-controller': 2.0.14 + '@smithy/types': 2.6.0 + tslib: 2.6.2 + dev: false + /@types/aws-lambda@8.10.125: resolution: {integrity: sha512-Vqw/WMlV4O1fJT6capim01v7VLDZkcX1n6Yhb52E7IfnMqYbNfwHfyDV8rRN42NLBtdDvfaqcCqs2K0fr5ljZw==} dev: false @@ -861,7 +1907,7 @@ packages: /@types/pako@2.0.2: resolution: {integrity: sha512-AtTbzIwhvLMTEUPudP3hxUwNK50DoX3amfVJmmL7WQH5iF3Kfqs8pG1tStsewHqmh75ULmjjldKn/B70D6DNcQ==} - dev: false + dev: true /@types/semver@7.5.1: resolution: {integrity: sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg==} @@ -980,6 +2026,10 @@ packages: engines: {node: '>=8'} dev: false + /bowser@2.11.0: + resolution: {integrity: sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1464,12 +2514,23 @@ packages: micromatch: 4.0.5 dev: false + /fast-xml-parser@4.2.5: + resolution: {integrity: sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g==} + hasBin: true + dependencies: + strnum: 1.0.5 + dev: false + /fastq@1.15.0: resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} dependencies: reusify: 1.0.4 dev: false + /fflate@0.8.1: + resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} + dev: false + /fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -2730,6 +3791,10 @@ packages: min-indent: 1.0.1 dev: false + /strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + dev: false + /sucrase@3.34.0: resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} engines: {node: '>=8'} @@ -2815,6 +3880,14 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: false + /tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: false + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + /tsup@7.2.0(typescript@5.2.2): resolution: {integrity: sha512-vDHlczXbgUvY3rWvqFEbSqmC1L7woozbzngMqTtL2PGBODTtWlRwGDDawhvWzr5c1QjKe4OAKqJGfE1xeXUvtQ==} engines: {node: '>=16.14'} @@ -3002,6 +4075,11 @@ packages: engines: {node: '>= 4.0.0'} dev: false + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + /validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} dependencies: diff --git a/test/compiler.test.app/src/todos/todos.fn.ts b/test/compiler.test.app/src/todos/todos.fn.ts index c2cfc81..bffb94f 100644 --- a/test/compiler.test.app/src/todos/todos.fn.ts +++ b/test/compiler.test.app/src/todos/todos.fn.ts @@ -1,5 +1,5 @@ -import { FnConfig, handle } from "@notation/aws/lambda"; -import { json } from "@notation/aws/api-gateway"; +import type { LambdaConfig } from "@notation/aws/lambda.fn"; +import { handle, json } from "@notation/aws/lambda.fn"; import { api } from "./utils"; const todos = await api.get("/todos"); @@ -12,7 +12,7 @@ export const getTodoCount = handle.apiRequest(() => { return json(todos.length); }); -export const config: FnConfig = { +export const config: LambdaConfig = { service: "aws/lambda", timeout: 5, memory: 64, diff --git a/test/compiler.test.ts b/test/compiler.test.ts index 4efedd5..da62b28 100644 --- a/test/compiler.test.ts +++ b/test/compiler.test.ts @@ -14,10 +14,11 @@ beforeAll(async () => { it("generates infra and runtime modules matching source file structure", async () => { const expected = [ "dist/infra/src/api.mjs", - "dist/runtime/src/todos/todos.fn.js", + "dist/runtime/src/todos/todos.fn/index.mjs", + "dist/runtime/src/todos/todos.fn/index.mjs.zip", ]; - const actual = await glob("dist/**/*.{js,mjs}", { cwd }); + const actual = await glob("dist/**/*.{js,mjs,zip}", { cwd }); - expect(actual).toEqual(expected); + expect(actual.sort()).toEqual(expected); });