├── .prettierrc ├── .husky └── pre-commit ├── packages ├── aws │ ├── src │ │ ├── lambda │ │ │ ├── index.ts │ │ │ └── lambda.ts │ │ ├── shared │ │ │ ├── index.ts │ │ │ └── lambda.handler.ts │ │ ├── event-bridge │ │ │ ├── index.ts │ │ │ ├── aws-conversions.ts │ │ │ ├── schedule.ts │ │ │ └── event-bridge-schedule.ts │ │ ├── api-gateway │ │ │ ├── index.ts │ │ │ ├── auth.ts │ │ │ ├── api.ts │ │ │ ├── utils.ts │ │ │ ├── router.ts │ │ │ └── route.ts │ │ └── lambda.fn │ │ │ ├── index.ts │ │ │ ├── config.ts │ │ │ ├── api-responses.ts │ │ │ └── handlers.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── test │ │ ├── api-gateway.test.ts │ │ └── lambda.fn.test.ts │ └── CHANGELOG.md ├── aws.iac │ ├── src │ │ ├── config.ts │ │ ├── resources │ │ │ ├── event-bridge │ │ │ │ ├── index.ts │ │ │ │ ├── lambda-permission.ts │ │ │ │ └── rule.ts │ │ │ ├── api-gateway │ │ │ │ ├── index.ts │ │ │ │ ├── auth.ts │ │ │ │ ├── stage.ts │ │ │ │ ├── api.ts │ │ │ │ └── route.ts │ │ │ └── lambda │ │ │ │ ├── index.ts │ │ │ │ ├── lambda-role-policy-attachment.ts │ │ │ │ ├── lambda-log-group.ts │ │ │ │ ├── lambda-role.ts │ │ │ │ └── lambda-api-permission.ts │ │ ├── templates │ │ │ ├── iam.managed-policy.ts │ │ │ ├── iam.policy.ts │ │ │ ├── iam.assume-role.ts │ │ │ └── arn.ts │ │ ├── client.ts │ │ ├── index.ts │ │ ├── context.ts │ │ └── utils │ │ │ ├── types.ts │ │ │ └── aws-clients.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ └── CHANGELOG.md ├── core │ ├── src │ │ ├── provisioner │ │ │ ├── index.ts │ │ │ ├── workflows │ │ │ │ ├── index.ts │ │ │ │ ├── workflow.destroy.ts │ │ │ │ ├── workflow.refresh.ts │ │ │ │ └── workflow.deploy.ts │ │ │ ├── operations │ │ │ │ ├── index.ts │ │ │ │ ├── operation.base.ts │ │ │ │ ├── operation.delete.ts │ │ │ │ ├── operation.update.ts │ │ │ │ ├── operation.read.ts │ │ │ │ └── operation.create.ts │ │ │ └── state.ts │ │ ├── orchestrator │ │ │ ├── state-getters.ts │ │ │ ├── state.ts │ │ │ ├── graph.ts │ │ │ ├── resource-group.ts │ │ │ └── resource.schema.ts │ │ ├── utils │ │ │ ├── paths.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ └── visualiser │ │ │ └── chart.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── CHANGELOG.md │ └── test │ │ ├── provisioner │ │ └── operation.create.test.ts │ │ ├── orchestrator │ │ ├── resource.doubles.ts │ │ ├── resource-group.test.ts │ │ └── resource.test.ts │ │ └── visualiser │ │ └── chart.test.ts ├── std.iac │ ├── src │ │ ├── index.ts │ │ ├── resources │ │ │ └── fs │ │ │ │ ├── index.ts │ │ │ │ ├── file.ts │ │ │ │ └── zip.ts │ │ └── utils │ │ │ ├── hash.ts │ │ │ └── zip.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ └── CHANGELOG.md ├── cli │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── src │ │ ├── destroy.ts │ │ ├── deploy.ts │ │ ├── visualise.ts │ │ ├── watch.ts │ │ ├── index.ts │ │ └── compile.ts │ ├── package.json │ └── CHANGELOG.md ├── dashboard │ ├── src │ │ ├── vite-env.d.ts │ │ ├── blocks │ │ │ ├── header.tsx │ │ │ ├── resource-list.tsx │ │ │ └── resource.tsx │ │ ├── main.tsx │ │ ├── hooks │ │ │ └── remote-state.ts │ │ ├── main.css │ │ └── app.tsx │ ├── tsup.config.ts │ ├── tsconfig.node.json │ ├── .gitignore │ ├── vite.config.ts │ ├── index.html │ ├── server │ │ ├── tsconfig.json │ │ └── server.ts │ ├── CHANGELOG.md │ ├── tsconfig.json │ └── package.json ├── create-notation │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── templates │ │ └── starter │ │ │ ├── runtime │ │ │ ├── utils.ts │ │ │ └── todos.fn.ts │ │ │ ├── infra │ │ │ └── api.ts │ │ │ ├── package.json │ │ │ └── tsconfig.json │ ├── src │ │ ├── index.ts │ │ └── scaffold.ts │ ├── package.json │ └── CHANGELOG.md ├── tsconfig │ ├── CHANGELOG.md │ ├── package.json │ └── base.json └── esbuild-plugins │ ├── src │ ├── index.ts │ ├── utils │ │ └── get-file.ts │ ├── plugins │ │ ├── function-runtime-plugin.ts │ │ └── function-infra-plugin.ts │ └── parsers │ │ ├── remove-config-export.ts │ │ ├── remove-unsafe-references.ts │ │ └── parse-fn-module.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ ├── package.json │ ├── test │ ├── esbuild-test-utils.ts │ ├── plugins │ │ ├── function-runtime-plugin.test.ts │ │ └── function-infra-plugin.test.ts │ └── parsers │ │ └── parse-fn-module.test.ts │ └── CHANGELOG.md ├── tsconfig.json ├── pnpm-workspace.yaml ├── .vscode └── extensions.json ├── examples ├── lambda-external │ ├── infra │ │ ├── index.ts │ │ ├── schedule.ts │ │ ├── api.ts │ │ └── lambda.ts │ ├── external │ │ ├── lambda.zip │ │ ├── lambda.mjs │ │ └── lambda.py │ ├── package.json │ └── tsconfig.json ├── api-gateway-authorizer │ ├── shared │ │ └── jwt.ts │ ├── tsconfig.json │ ├── infra │ │ └── api.ts │ ├── runtime │ │ └── user.fn.ts │ └── package.json ├── api-gateway │ ├── tsconfig.json │ ├── runtime │ │ ├── utils.ts │ │ └── todos.fn.ts │ ├── infra │ │ └── api.ts │ └── package.json └── event-bridge │ ├── tsconfig.json │ ├── infra │ └── schedule.ts │ ├── runtime │ └── log-event.fn.ts │ └── package.json ├── .github ├── assets │ ├── code-error.png │ ├── code-infra-graph.png │ └── video-thumbnail.png └── workflows │ └── pr.test.yaml ├── vitest.config.ts ├── turbo.json ├── .changeset ├── config.json └── README.md ├── CONTRIBUTING.md ├── test └── compiler.test.ts ├── package.json ├── .gitignore └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm run test:once 2 | -------------------------------------------------------------------------------- /packages/aws/src/lambda/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lambda"; 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/aws.iac/src/config.ts: -------------------------------------------------------------------------------- 1 | export const region = "us-west-2"; 2 | -------------------------------------------------------------------------------- /packages/aws/src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lambda.handler"; 2 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./workflows"; 2 | -------------------------------------------------------------------------------- /packages/std.iac/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as fs from "./resources/fs"; 2 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/dashboard/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "examples/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/lambda-external/infra/index.ts: -------------------------------------------------------------------------------- 1 | import "./api"; 2 | import "./schedule"; 3 | -------------------------------------------------------------------------------- /packages/create-notation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /packages/std.iac/src/resources/fs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./file"; 2 | export * from "./zip"; 3 | -------------------------------------------------------------------------------- /.github/assets/code-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notation-dev/notation/HEAD/.github/assets/code-error.png -------------------------------------------------------------------------------- /packages/aws/src/event-bridge/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./event-bridge-schedule"; 2 | export * from "./schedule"; 3 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/event-bridge/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./rule"; 2 | export * from "./lambda-permission"; 3 | -------------------------------------------------------------------------------- /packages/aws/src/api-gateway/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./router"; 3 | export * from "./auth"; 4 | -------------------------------------------------------------------------------- /packages/tsconfig/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # tsconfig 2 | 3 | ## 0.1.0 4 | 5 | ### Minor Changes 6 | 7 | - Fix package versions 8 | -------------------------------------------------------------------------------- /.github/assets/code-infra-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notation-dev/notation/HEAD/.github/assets/code-infra-graph.png -------------------------------------------------------------------------------- /.github/assets/video-thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notation-dev/notation/HEAD/.github/assets/video-thumbnail.png -------------------------------------------------------------------------------- /packages/aws/src/lambda.fn/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api-responses"; 2 | export * from "./config"; 3 | export * from "./handlers"; 4 | -------------------------------------------------------------------------------- /packages/aws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/aws.iac/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/std.iac/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/api-gateway-authorizer/shared/jwt.ts: -------------------------------------------------------------------------------- 1 | export type JWTClaims = { 2 | iss: string; 3 | sub: string; 4 | $username: string; 5 | }; 6 | -------------------------------------------------------------------------------- /examples/lambda-external/external/lambda.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notation-dev/notation/HEAD/examples/lambda-external/external/lambda.zip -------------------------------------------------------------------------------- /packages/esbuild-plugins/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./plugins/function-infra-plugin"; 2 | export * from "./plugins/function-runtime-plugin"; 3 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": "." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/aws/src/lambda.fn/config.ts: -------------------------------------------------------------------------------- 1 | export type LambdaConfig = { 2 | service: "aws/lambda"; 3 | memory?: number; 4 | timeout?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /packages/aws/src/lambda.fn/api-responses.ts: -------------------------------------------------------------------------------- 1 | export const json = (result: any) => ({ 2 | body: JSON.stringify(result), 3 | statusCode: 200, 4 | }); 5 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/workflows/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./workflow.deploy"; 2 | export * from "./workflow.destroy"; 3 | export * from "./workflow.refresh"; 4 | -------------------------------------------------------------------------------- /packages/aws.iac/src/templates/iam.managed-policy.ts: -------------------------------------------------------------------------------- 1 | export const AWSLambdaBasicExecutionRole = 2 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"; 3 | -------------------------------------------------------------------------------- /packages/aws.iac/src/client.ts: -------------------------------------------------------------------------------- 1 | import { ResourceGroup } from "@notation/core"; 2 | 3 | export class AwsResourceGroup extends ResourceGroup { 4 | platform = "aws"; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | dts: true, 6 | format: ["esm"], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/cli/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["esm"], 6 | platform: "node", 7 | }); 8 | -------------------------------------------------------------------------------- /packages/dashboard/src/blocks/header.tsx: -------------------------------------------------------------------------------- 1 | import { logo } from "./logo"; 2 | 3 | export const Header = () => ( 4 |
{logo}
5 | ); 6 | -------------------------------------------------------------------------------- /packages/dashboard/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["server/server.ts"], 5 | dts: true, 6 | format: ["esm"], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | dts: true, 6 | format: ["esm"], 7 | }); 8 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/api-gateway/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export * from "./lambda-integration"; 3 | export * from "./route"; 4 | export * from "./stage"; 5 | export * from "./auth"; 6 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/operations/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./operation.create"; 2 | export * from "./operation.delete"; 3 | export * from "./operation.read"; 4 | export * from "./operation.update"; 5 | -------------------------------------------------------------------------------- /packages/create-notation/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | format: ["esm"], 6 | platform: "node", 7 | }); 8 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/aws.iac/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | dts: true, 6 | format: ["esm"], 7 | platform: "node", 8 | }); 9 | -------------------------------------------------------------------------------- /packages/core/src/orchestrator/state-getters.ts: -------------------------------------------------------------------------------- 1 | import { resourceGroups, resources } from "./state"; 2 | 3 | export const getResourceGroups = () => resourceGroups; 4 | export const getResources = () => resources; 5 | -------------------------------------------------------------------------------- /packages/std.iac/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts"], 5 | dts: true, 6 | format: ["esm"], 7 | platform: "node", 8 | }); 9 | -------------------------------------------------------------------------------- /packages/aws.iac/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./client"; 2 | export * as apiGateway from "./resources/api-gateway"; 3 | export * as lambda from "./resources/lambda"; 4 | export * as eventBridge from "./resources/event-bridge"; 5 | -------------------------------------------------------------------------------- /examples/api-gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "runtime/*": ["./runtime/*"], 6 | "infra/*": ["./infra/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/event-bridge/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "runtime/*": ["./runtime/*"], 6 | "infra/*": ["./infra/*"] 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/lambda-external/external/lambda.mjs: -------------------------------------------------------------------------------- 1 | export const handler = async (event) => { 2 | console.log("event", event); 3 | return { 4 | statusCode: 200, 5 | body: JSON.stringify({ message: "Hello, world!" }), 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from "vitest/config"; 4 | import tsconfigPaths from "vite-tsconfig-paths"; 5 | 6 | export default defineConfig({ 7 | plugins: [tsconfigPaths()], 8 | }); 9 | -------------------------------------------------------------------------------- /examples/api-gateway/runtime/utils.ts: -------------------------------------------------------------------------------- 1 | export const api = { 2 | get: async (path: string) => { 3 | const res = await fetch(`https://jsonplaceholder.typicode.com${path}`); 4 | return res.json() as Promise; 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /examples/lambda-external/external/lambda.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def handler(event, context): 4 | print("event", event) 5 | return { 6 | 'statusCode': 200, 7 | 'body': json.dumps({'message': 'Hello, world!'}) 8 | } 9 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/lambda/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lambda-api-permission"; 2 | export * from "./lambda-log-group"; 3 | export * from "./lambda-role-policy-attachment"; 4 | export * from "./lambda-role"; 5 | export * from "./lambda"; 6 | -------------------------------------------------------------------------------- /packages/cli/src/destroy.ts: -------------------------------------------------------------------------------- 1 | import { destroyApp } from "@notation/core"; 2 | import { compile } from "./compile"; 3 | 4 | export async function destroy(entryPoint: string) { 5 | await compile(entryPoint); 6 | await destroyApp(entryPoint); 7 | } 8 | -------------------------------------------------------------------------------- /packages/create-notation/templates/starter/runtime/utils.ts: -------------------------------------------------------------------------------- 1 | export const api = { 2 | get: async (path: string) => { 3 | const res = await fetch(`https://jsonplaceholder.typicode.com${path}`); 4 | return res.json() as Promise; 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/create-notation/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { scaffoldApp } from "./scaffold"; 3 | 4 | const argv = process.argv.slice(2).filter((arg) => arg !== "--"); 5 | const appName = argv[0] ?? "notation-starter"; 6 | 7 | await scaffoldApp(appName); 8 | -------------------------------------------------------------------------------- /examples/api-gateway-authorizer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "runtime/*": ["./runtime/*"], 6 | "infra/*": ["./infra/*"], 7 | "shared/*": ["./shared/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/event-bridge/infra/schedule.ts: -------------------------------------------------------------------------------- 1 | import * as eventBridge from "@notation/aws/event-bridge"; 2 | import { logEvent } from "runtime/log-event.fn"; 3 | 4 | eventBridge.schedule({ 5 | name: "log-every-minute", 6 | schedule: eventBridge.rate(1, "minute"), 7 | handler: logEvent, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/create-notation/templates/starter/infra/api.ts: -------------------------------------------------------------------------------- 1 | import { api, router } from "@notation/aws/api-gateway"; 2 | import { getTodos } from "runtime/todos.fn"; 3 | 4 | const todoApi = api({ name: "todo-api" }); 5 | const todoRouter = router(todoApi); 6 | 7 | todoRouter.get("/todos", getTodos); 8 | -------------------------------------------------------------------------------- /packages/dashboard/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/lambda-external/infra/schedule.ts: -------------------------------------------------------------------------------- 1 | import { schedule, rate } from "@notation/aws/event-bridge"; 2 | import { externalJsLambda } from "./lambda"; 3 | 4 | export const myschedule = schedule({ 5 | name: "my-schedule", 6 | schedule: rate(1, "minute"), 7 | handler: externalJsLambda, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/core/src/utils/paths.ts: -------------------------------------------------------------------------------- 1 | export const filePaths = { 2 | dist: { 3 | runtime: { 4 | index: (entryPoint: string) => 5 | `dist/${entryPoint.replace(/.ts$/, "/index.mjs")}`, 6 | }, 7 | infra: (entryPoint: string) => `dist/${entryPoint.replace(/.ts$/, ".mjs")}`, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/dashboard/src/main.tsx: -------------------------------------------------------------------------------- 1 | import "./main.css"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom/client"; 4 | import App from "./app.tsx"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root")!).render( 7 | 8 | 9 | , 10 | ); 11 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "outputs": ["dist/**"], 6 | "dependsOn": ["^build"] 7 | }, 8 | "dev": { 9 | "cache": false, 10 | "persistent": true 11 | }, 12 | "typecheck": {}, 13 | "lint": {} 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/api-gateway/infra/api.ts: -------------------------------------------------------------------------------- 1 | import { api, router } from "@notation/aws/api-gateway"; 2 | import { getTodos, getTodoCount } from "runtime/todos.fn"; 3 | 4 | const todoApi = api({ name: "todo-api" }); 5 | const todoRouter = router(todoApi); 6 | 7 | todoRouter.get("/todos", getTodos); 8 | todoRouter.get("/todos/count", getTodoCount); 9 | -------------------------------------------------------------------------------- /packages/aws.iac/src/templates/iam.policy.ts: -------------------------------------------------------------------------------- 1 | export const lambdaTrustPolicy = { 2 | Version: "2012-10-17", 3 | Statement: [ 4 | { 5 | Sid: "AllowAssumeRole", 6 | Effect: "Allow", 7 | Principal: { 8 | Service: "lambda.amazonaws.com", 9 | }, 10 | Action: "sts:AssumeRole", 11 | }, 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [["@notation/*"]], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /examples/event-bridge/runtime/log-event.fn.ts: -------------------------------------------------------------------------------- 1 | import { LambdaConfig, handle } from "@notation/aws/lambda.fn"; 2 | 3 | export const logEvent = handle.eventBridgeScheduledEvent((event) => { 4 | console.log(JSON.stringify(event)); 5 | }); 6 | 7 | export const config: LambdaConfig = { 8 | service: "aws/lambda", 9 | timeout: 5, 10 | memory: 64, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/aws.iac/src/context.ts: -------------------------------------------------------------------------------- 1 | import { GetCallerIdentityCommand } from "@aws-sdk/client-sts"; 2 | import { stsClient } from "src/utils/aws-clients"; 3 | 4 | const command = new GetCallerIdentityCommand({}); 5 | 6 | export const getAwsAccountId = async () => { 7 | const response = await stsClient.send(command); 8 | return response.Account; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./provisioner"; 2 | export * from "./orchestrator/graph"; 3 | export * from "./utils/paths"; 4 | export * from "./visualiser/chart"; 5 | export * from "./orchestrator/resource"; 6 | export * from "./orchestrator/resource-group"; 7 | export * from "./orchestrator/state-getters"; 8 | export { reset } from "./orchestrator/state"; 9 | -------------------------------------------------------------------------------- /packages/aws.iac/src/templates/iam.assume-role.ts: -------------------------------------------------------------------------------- 1 | export function assumeRolePolicyForPrincipal(principal: string) { 2 | return { 3 | Version: "2012-10-17", 4 | Statement: [ 5 | { 6 | Sid: "AllowAssumeRole", 7 | Effect: "Allow", 8 | Principal: principal, 9 | Action: "sts:AssumeRole", 10 | }, 11 | ], 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/std.iac/src/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import crypto from "crypto"; 2 | import fs from "node:fs/promises"; 3 | 4 | export const getSourceSha256 = async (filePath: string) => { 5 | const sourceBuffer = await fs.readFile(filePath); 6 | const sourceSha256 = crypto 7 | .createHash("sha256") 8 | .update(sourceBuffer) 9 | .digest("hex"); 10 | return sourceSha256; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/aws/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: [ 5 | "src/api-gateway/index.ts", 6 | "src/event-bridge/index.ts", 7 | "src/lambda/index.ts", 8 | "src/lambda.fn/index.ts", 9 | "src/shared/index.ts", 10 | ], 11 | splitting: false, 12 | dts: true, 13 | format: ["esm"], 14 | platform: "node", 15 | }); 16 | -------------------------------------------------------------------------------- /packages/dashboard/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react(), tailwindcss()], 8 | 9 | server: { 10 | proxy: { 11 | "/state": "http://localhost:6682", 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Publishing Packages 4 | 5 | Create a changeset, when a change should result in a version bump: 6 | 7 | ``` 8 | npm run changeset 9 | ``` 10 | 11 | From the main branch, consolidate changesets, bumping the versions of affected packages: 12 | 13 | ``` 14 | npm run version 15 | ``` 16 | 17 | Then publish to the NPM registry: 18 | 19 | ``` 20 | npm run release 21 | ``` 22 | -------------------------------------------------------------------------------- /packages/create-notation/templates/starter/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "notation-starter", 4 | "scripts": { 5 | "compile": "notation compile infra/api.ts", 6 | "dashboard": "notation dashboard", 7 | "deploy": "notation deploy infra/api.ts", 8 | "destroy": "notation destroy infra/api.ts", 9 | "viz": "notation viz infra/api.ts", 10 | "watch": "notation watch infra/api.ts" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/aws/src/api-gateway/auth.ts: -------------------------------------------------------------------------------- 1 | export type JWTAuthorizerConfig = { 2 | type: "jwt"; 3 | issuer: string; 4 | audience: string[]; 5 | scopes: string[]; 6 | }; 7 | 8 | export type Unauthenticated = { 9 | type: "NONE"; 10 | scopes: string[]; 11 | }; 12 | 13 | export type AuthorizerConfig = JWTAuthorizerConfig | Unauthenticated; 14 | 15 | export const NO_AUTH = { 16 | type: "NONE", 17 | scopes: [] as string[], 18 | } as const; 19 | -------------------------------------------------------------------------------- /packages/dashboard/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Notation Dashboard 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/lambda-external/infra/api.ts: -------------------------------------------------------------------------------- 1 | import { api, router } from "@notation/aws/api-gateway"; 2 | import { 3 | externalJsLambda, 4 | externalZipLambda, 5 | externalPyLambda, 6 | } from "./lambda"; 7 | 8 | const helloApi = api({ name: "hello-api" }); 9 | const helloRouter = router(helloApi); 10 | 11 | helloRouter.get("/hello1", externalJsLambda); 12 | helloRouter.get("/hello2", externalZipLambda); 13 | helloRouter.get("/hello3", externalPyLambda); 14 | -------------------------------------------------------------------------------- /packages/create-notation/templates/starter/runtime/todos.fn.ts: -------------------------------------------------------------------------------- 1 | import type { LambdaConfig } from "@notation/aws/lambda.fn"; 2 | import { handle, json } from "@notation/aws/lambda.fn"; 3 | import { api } from "./utils"; 4 | 5 | const todos = await api.get("/todos"); 6 | 7 | export const getTodos = handle.apiRequest(() => { 8 | return json(todos); 9 | }); 10 | 11 | export const config: LambdaConfig = { 12 | service: "aws/lambda", 13 | timeout: 5, 14 | memory: 64, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/std.iac/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@notation/std.iac", 4 | "version": "0.11.1", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "tsup --clean", 12 | "dev": "tsup --watch", 13 | "typecheck": "tsc --noEmit" 14 | }, 15 | "dependencies": { 16 | "@notation/core": "workspace:*", 17 | "fflate": "0.8.2", 18 | "zod": "^3.24.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/aws.iac/src/templates/arn.ts: -------------------------------------------------------------------------------- 1 | import { getAwsAccountId } from "src/context"; 2 | import { region } from "src/config"; 3 | 4 | export const getLambdaInvocationUri = (arn: string) => { 5 | return `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations`; 6 | }; 7 | 8 | export const generateApiGatewaySourceArn = async (apiId: string) => { 9 | const accountId = await getAwsAccountId(); 10 | return `arn:aws:execute-api:${region}:${accountId}:${apiId}/*/*`; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/create-notation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "create-notation", 4 | "version": "0.2.0", 5 | "bin": { 6 | "create-notation": "dist/index.js" 7 | }, 8 | "scripts": { 9 | "build": "tsup --clean", 10 | "dev": "tsup --watch" 11 | }, 12 | "dependencies": { 13 | "fs-extra": "^11.3.0", 14 | "which-pm-runs": "^1.1.0" 15 | }, 16 | "devDependencies": { 17 | "@types/fs-extra": "^11.0.4", 18 | "@types/which-pm-runs": "^1.0.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/dashboard/server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "outDir": "../dist/server", 6 | "moduleResolution": "NodeNext", 7 | "module": "NodeNext", 8 | "skipLibCheck": true 9 | }, 10 | "files": ["server.ts"], 11 | "include": ["**/*.ts"], 12 | "exclude": [ 13 | "node_modules", 14 | "../node_modules", 15 | "../dist", 16 | "../../node_modules", 17 | "../../../node_modules" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/orchestrator/state.ts: -------------------------------------------------------------------------------- 1 | import { BaseResource } from "./resource"; 2 | import { ResourceGroup } from "./resource-group"; 3 | 4 | export let resourceGroups: ResourceGroup[] = []; 5 | export let resources: BaseResource[] = []; 6 | 7 | let resourceGroupCounter = -1; 8 | 9 | export const getNextResourceGroupCount = () => { 10 | return ++resourceGroupCounter; 11 | }; 12 | 13 | export const reset = () => { 14 | resources = []; 15 | resourceGroups = []; 16 | resourceGroupCounter = -1; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/dashboard/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @notation/dashboard 2 | 3 | ## 0.11.1 4 | 5 | ### Patch Changes 6 | 7 | - Hide large buffers from state file 8 | 9 | ## 0.11.0 10 | 11 | ## 0.10.0 12 | 13 | ### Minor Changes 14 | 15 | - Fix package versions 16 | 17 | ## 0.3.0 18 | 19 | ### Minor Changes 20 | 21 | - Fix dashboard 22 | 23 | ## 0.2.0 24 | 25 | ### Minor Changes 26 | 27 | - 5debdd1: Show deployed resource state in dashboard 28 | 29 | ## 0.1.0 30 | 31 | ### Minor Changes 32 | 33 | - Release dashboard 34 | -------------------------------------------------------------------------------- /examples/lambda-external/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "lambda", 4 | "scripts": { 5 | "compile": "notation compile infra/index.ts", 6 | "dashboard": "notation dashboard", 7 | "deploy": "notation deploy infra/index.ts", 8 | "destroy": "notation destroy infra/index.ts", 9 | "viz": "notation viz infra/index.ts", 10 | "watch": "notation watch infra/index.ts" 11 | }, 12 | "dependencies": { 13 | "@notation/aws": "workspace:*", 14 | "@notation/cli": "workspace:*" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/api-gateway-authorizer/infra/api.ts: -------------------------------------------------------------------------------- 1 | import { api, router } from "@notation/aws/api-gateway"; 2 | import { getUserHandler } from "runtime/user.fn"; 3 | import type { JWTClaims } from "shared/jwt"; 4 | 5 | const userApi = api({ name: "user-api-jwt" }); 6 | 7 | const userApiRouter = router(userApi).withJWTAuthorizer({ 8 | type: "jwt", 9 | issuer: "https://myuseraccount.uk.auth0.com/", 10 | audience: ["https://auth0-jwt-authorizer"], 11 | scopes: [], 12 | }); 13 | 14 | userApiRouter.get("/user", getUserHandler); 15 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /examples/api-gateway-authorizer/runtime/user.fn.ts: -------------------------------------------------------------------------------- 1 | import type { LambdaConfig } from "@notation/aws/lambda.fn"; 2 | import { handle, json } from "@notation/aws/lambda.fn"; 3 | import type { JWTClaims } from "shared/jwt"; 4 | 5 | export const getUserHandler = handle.jwtAuthorizedApiRequest( 6 | async (event) => { 7 | return json({ 8 | message: event.requestContext.authorizer.jwt.claims.$username, 9 | }); 10 | }, 11 | ); 12 | 13 | export const config: LambdaConfig = { 14 | service: "aws/lambda", 15 | timeout: 5, 16 | memory: 64, 17 | }; 18 | -------------------------------------------------------------------------------- /examples/api-gateway/runtime/todos.fn.ts: -------------------------------------------------------------------------------- 1 | import type { LambdaConfig } from "@notation/aws/lambda.fn"; 2 | import { handle, json } from "@notation/aws/lambda.fn"; 3 | import { api } from "./utils"; 4 | 5 | const todos = await api.get("/todos"); 6 | 7 | export const getTodos = handle.apiRequest(() => { 8 | return json(todos); 9 | }); 10 | 11 | export const getTodoCount = handle.apiRequest(() => { 12 | return json(todos.length); 13 | }); 14 | 15 | export const config: LambdaConfig = { 16 | service: "aws/lambda", 17 | timeout: 5, 18 | memory: 64, 19 | }; 20 | -------------------------------------------------------------------------------- /packages/dashboard/src/hooks/remote-state.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { BaseResource } from "@notation/core"; 3 | 4 | export const useRemoteState = () => { 5 | const [state, setState] = useState({}); 6 | 7 | useEffect(() => { 8 | const evtSource = new EventSource("/state"); 9 | 10 | evtSource.onmessage = function (event) { 11 | setState(JSON.parse(event.data) ?? {}); 12 | }; 13 | 14 | return () => { 15 | evtSource.close(); 16 | }; 17 | }, []); 18 | 19 | return state as Record; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/src/utils/get-file.ts: -------------------------------------------------------------------------------- 1 | export type GetFile = ( 2 | filePath: string, 3 | ) => string | Promise | undefined; 4 | 5 | export const fsGetFile = async (filePath: string) => { 6 | const fs = await import("fs/promises"); 7 | return fs.readFile(filePath, { encoding: "utf-8" }); 8 | }; 9 | 10 | export const withFileCheck = (getFile: GetFile) => (filePath: string) => { 11 | const fileContent = getFile(filePath); 12 | if (!fileContent) { 13 | throw new Error(`Module ${filePath} is empty or does not exist`); 14 | } 15 | return fileContent; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/std.iac/src/utils/zip.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import * as fs from "node:fs/promises"; 3 | import * as fflate from "fflate"; 4 | 5 | export const zip = { 6 | package: async (sourceFilePath: string, filePath: string) => { 7 | const inputFile = await fs.readFile(sourceFilePath); 8 | const fileName = path.basename(sourceFilePath); 9 | const archive = fflate.zipSync( 10 | { [fileName]: inputFile }, 11 | // ensure deterministic output 12 | { level: 9, mtime: "0/0/00 00:00 PM" }, 13 | ); 14 | await fs.writeFile(filePath, archive); 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/create-notation/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # create-notation 2 | 3 | ## 0.2.0 4 | 5 | ### Minor Changes 6 | 7 | - Fix package versions 8 | 9 | ## 0.1.0 10 | 11 | ### Minor Changes 12 | 13 | - 2a6fc59: Add optional JWT authorizer config to route resource 14 | 15 | ## 0.0.5 16 | 17 | ### Patch Changes 18 | 19 | - Fix updating std.zip resource 20 | 21 | ## 0.0.3 22 | 23 | ### Patch Changes 24 | 25 | - Add hashbang 26 | 27 | ## 0.0.2 28 | 29 | ### Patch Changes 30 | 31 | - 10a8133: Migrate project scaffolding to create-notation 32 | 33 | ## 0.0.1 34 | 35 | ### Patch Changes 36 | 37 | - Create package 38 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@notation/esbuild-plugins", 4 | "version": "0.11.1", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "tsup --clean", 12 | "dev": "tsup --watch", 13 | "typecheck": "tsc --noEmit" 14 | }, 15 | "dependencies": { 16 | "@notation/core": "workspace:*", 17 | "esbuild": "^0.25.0", 18 | "typescript": "^5.7.3" 19 | }, 20 | "devDependencies": { 21 | "@types/common-tags": "^1.8.4", 22 | "common-tags": "^1.8.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/compiler.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, expect, it } from "vitest"; 2 | import { $ } from "execa"; 3 | import path from "path"; 4 | import { glob } from "glob"; 5 | 6 | const cwd = path.join(process.cwd(), "examples/api-gateway"); 7 | const $$ = $({ cwd }); 8 | 9 | beforeAll(async () => { 10 | await $$`rm -rf dist`; 11 | await $$`npm run compile`; 12 | }); 13 | 14 | it("generates infra and runtime modules matching source file structure", async () => { 15 | const expected = ["dist/infra/api.mjs", "dist/runtime/todos.fn/index.mjs"]; 16 | const actual = await glob("dist/**/*.{js,mjs,zip}", { cwd }); 17 | expect(actual.sort()).toEqual(expected); 18 | }); 19 | -------------------------------------------------------------------------------- /examples/api-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "api-demo", 4 | "scripts": { 5 | "compile": "notation compile infra/api.ts", 6 | "dashboard": "notation dashboard", 7 | "deploy": "notation deploy infra/api.ts", 8 | "destroy": "notation destroy infra/api.ts", 9 | "viz": "notation viz infra/api.ts", 10 | "watch": "notation watch infra/api.ts", 11 | "typecheck": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@notation/aws": "workspace:*", 15 | "@notation/cli": "workspace:*", 16 | "@notation/core": "workspace:*" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^22.13.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/aws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@notation/aws", 4 | "version": "0.11.1", 5 | "exports": { 6 | "./*": { 7 | "import": "./dist/*/index.js", 8 | "types": "./dist/*/index.d.ts" 9 | } 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "build": "tsup --clean", 16 | "dev": "tsup --watch", 17 | "typecheck": "tsc --noEmit" 18 | }, 19 | "dependencies": { 20 | "@notation/aws.iac": "workspace:*", 21 | "@notation/core": "workspace:*", 22 | "@notation/std.iac": "workspace:*", 23 | "@types/aws-lambda": "^8.10.147", 24 | "aws-jwt-verify": "^5.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/api-gateway-authorizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "api-authorizer-demo", 4 | "scripts": { 5 | "compile": "notation compile infra/api.ts", 6 | "dashboard": "notation dashboard", 7 | "deploy": "notation deploy infra/api.ts", 8 | "destroy": "notation destroy infra/api.ts", 9 | "viz": "notation viz infra/api.ts", 10 | "watch": "notation watch infra/api.ts", 11 | "typecheck": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@notation/aws": "workspace:*", 15 | "@notation/cli": "workspace:*", 16 | "@notation/core": "workspace:*" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^22.13.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/event-bridge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "schedule-demo", 4 | "scripts": { 5 | "compile": "notation compile infra/schedule.ts", 6 | "dashboard": "notation dashboard", 7 | "deploy": "notation deploy infra/schedule.ts", 8 | "destroy": "notation destroy infra/schedule.ts", 9 | "viz": "notation viz infra/schedule.ts", 10 | "watch": "notation watch infra/schedule.ts", 11 | "typecheck": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@notation/aws": "workspace:*", 15 | "@notation/cli": "workspace:*", 16 | "@notation/core": "workspace:*" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^22.13.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/lambda-external/infra/lambda.ts: -------------------------------------------------------------------------------- 1 | import { lambda } from "@notation/aws/lambda"; 2 | 3 | export const externalJsLambda = lambda({ 4 | id: "external-js", 5 | handler: "handler", 6 | code: { 7 | type: "file", 8 | path: "external/lambda.mjs", 9 | }, 10 | }); 11 | 12 | export const externalZipLambda = lambda({ 13 | id: "external-zip", 14 | handler: "handler", 15 | code: { 16 | type: "zip", 17 | path: "external/lambda.zip", 18 | }, 19 | }); 20 | 21 | export const externalPyLambda = lambda({ 22 | id: "external-py", 23 | handler: "handler", 24 | code: { 25 | type: "file", 26 | path: "external/lambda.py", 27 | }, 28 | runtime: "python3.12", 29 | }); 30 | -------------------------------------------------------------------------------- /packages/core/src/orchestrator/graph.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { 3 | getResources, 4 | getResourceGroups, 5 | } from "src/orchestrator/state-getters"; 6 | import { filePaths } from "src/utils/paths"; 7 | import { reset } from ".."; 8 | 9 | export async function getResourceGraph(entryPoint: string) { 10 | reset(); 11 | const outFilePath = filePaths.dist.infra(entryPoint); 12 | 13 | // todo: move into worker thread. this will cause memory leaks 14 | await import(path.join(process.cwd(), `${outFilePath}?${Date.now()}`)); 15 | 16 | const resourceGroups = getResourceGroups(); 17 | const resources = getResources(); 18 | 19 | return { resourceGroups, resources }; 20 | } 21 | -------------------------------------------------------------------------------- /packages/aws/src/api-gateway/api.ts: -------------------------------------------------------------------------------- 1 | import * as aws from "@notation/aws.iac"; 2 | 3 | export const api = (rgConfig: { name: string }) => { 4 | const apiGroup = new aws.AwsResourceGroup("API Gateway", rgConfig); 5 | 6 | const apiResource = apiGroup.add( 7 | new aws.apiGateway.Api({ 8 | id: rgConfig.name, 9 | config: { 10 | Name: rgConfig.name, 11 | ProtocolType: "HTTP", 12 | }, 13 | }), 14 | ); 15 | 16 | apiGroup.add( 17 | new aws.apiGateway.Stage({ 18 | id: `${rgConfig.name}-stage`, 19 | config: { StageName: "$default", AutoDeploy: true }, 20 | dependencies: { api: apiResource }, 21 | }), 22 | ); 23 | 24 | return apiGroup; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/aws.iac/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fix AWS SDK types 3 | * For create and update: remove undefined from value 4 | * For read: make all options required 5 | */ 6 | export type AwsSchema = { 7 | Key: S["Key"]; 8 | CreateParams: NonUndefined; 9 | UpdateParams: S["UpdateParams"] extends undefined 10 | ? NonUndefined 11 | : never; 12 | ReadResult: S["ReadResult"] extends undefined ? {} : S["ReadResult"]; 13 | }; 14 | 15 | type NonUndefined = { 16 | [P in keyof T]: Exclude; 17 | }; 18 | 19 | type SdkSchema = { 20 | Key: any; 21 | CreateParams: any; 22 | UpdateParams?: any; 23 | ReadResult?: any; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/aws/src/api-gateway/utils.ts: -------------------------------------------------------------------------------- 1 | import { AuthorizerConfig, JWTAuthorizerConfig } from "./auth"; 2 | 3 | export const mapAuthConfig = ( 4 | apiId: string, 5 | method: string, 6 | path: string, 7 | config: JWTAuthorizerConfig, 8 | ) => { 9 | const jwtType: "JWT" = "JWT"; 10 | 11 | return { 12 | Name: `${apiId}_${method}_${path.replace("/", "")}_authorizer`, 13 | AuthorizerType: jwtType, 14 | IdentitySource: ["$request.header.Authorization"], 15 | JwtConfiguration: { 16 | Audience: config.audience, 17 | Issuer: config.issuer, 18 | }, 19 | }; 20 | }; 21 | 22 | export const mapAuthType = (config: AuthorizerConfig) => { 23 | return config?.type === "jwt" ? "JWT" : "NONE"; 24 | }; 25 | -------------------------------------------------------------------------------- /packages/dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@notation/cli", 4 | "version": "0.11.1", 5 | "files": [ 6 | "dist", 7 | "templates" 8 | ], 9 | "bin": { 10 | "notation": "dist/index.js" 11 | }, 12 | "scripts": { 13 | "build": "tsup --clean", 14 | "dev": "tsup --watch", 15 | "typecheck": "tsc --noEmit" 16 | }, 17 | "dependencies": { 18 | "@notation/core": "workspace:*", 19 | "@notation/dashboard": "workspace:*", 20 | "@notation/esbuild-plugins": "workspace:*", 21 | "chokidar": "^4.0.3", 22 | "commander": "^13.1.0", 23 | "esbuild": "^0.25.0", 24 | "glob": "^11.0.1" 25 | }, 26 | "devDependencies": { 27 | "@types/which-pm-runs": "^1.0.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/cli/src/deploy.ts: -------------------------------------------------------------------------------- 1 | import { deployApp } from "@notation/core"; 2 | import { compile } from "./compile"; 3 | 4 | export async function deploy(entryPoint: string) { 5 | await compile(entryPoint); 6 | console.log(`Deploying ${entryPoint}`); 7 | 8 | try { 9 | await deployApp(entryPoint); 10 | } catch (err: any) { 11 | if (err.name === "CredentialsProviderError") { 12 | console.log( 13 | "\nAWS credentials not found.", 14 | "\n\nEnsure you have a default profile set up in ~/.aws/credentials.", 15 | "\n\nIf using another profile run AWS_PROFILE=otherProfile notation deploy.\n", 16 | ); 17 | process.exit(1); 18 | } 19 | console.log(err); 20 | process.exit(1); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/cli/src/visualise.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createMermaidFlowChart, 3 | createMermaidLiveUrl, 4 | getResourceGraph, 5 | } from "@notation/core"; 6 | import { log } from "console"; 7 | import { compileInfra } from "./compile"; 8 | 9 | export async function visualise(entryPoint: string) { 10 | await compileInfra(entryPoint); 11 | await generateGraph(entryPoint); 12 | } 13 | 14 | export async function generateGraph(entryPoint: string) { 15 | log(`Generating graph for ${entryPoint}`); 16 | 17 | const graph = await getResourceGraph(entryPoint); 18 | const chart = createMermaidFlowChart(graph.resourceGroups, graph.resources); 19 | const chartUrl = createMermaidLiveUrl(chart); 20 | 21 | log("\nGenerated infrastructure chart:\n"); 22 | log(chartUrl); 23 | } 24 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "lib": ["esnext"], 6 | "module": "esnext", 7 | "target": "esnext", 8 | 9 | "moduleResolution": "bundler", 10 | "noEmit": true, 11 | 12 | "composite": false, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "inlineSources": false, 17 | "isolatedModules": true, 18 | "noUnusedLocals": false, 19 | "noUnusedParameters": false, 20 | "preserveWatchOutput": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true, 23 | "strict": true, 24 | "strictNullChecks": true 25 | }, 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@notation/core", 4 | "version": "0.11.1", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "build": "tsup --clean", 12 | "dev": "tsup --watch", 13 | "typecheck": "tsc --noEmit" 14 | }, 15 | "dependencies": { 16 | "deep-object-diff": "^1.1.9", 17 | "fs-extra": "^11.3.0", 18 | "js-base64": "^3.7.7", 19 | "lodash-es": "^4.17.21", 20 | "pako": "^2.1.0", 21 | "zod": "^3.24.2" 22 | }, 23 | "devDependencies": { 24 | "@types/common-tags": "^1.8.4", 25 | "@types/fs-extra": "^11.0.4", 26 | "@types/lodash-es": "^4.17.12", 27 | "@types/pako": "^2.0.3", 28 | "common-tags": "^1.8.2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/aws.iac/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module", 3 | "name": "@notation/aws.iac", 4 | "version": "0.11.1", 5 | "scripts": { 6 | "build": "tsup --clean", 7 | "dev": "tsup --watch", 8 | "typecheck": "tsc --noEmit" 9 | }, 10 | "main": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "files": [ 13 | "dist" 14 | ], 15 | "dependencies": { 16 | "@aws-sdk/client-apigatewayv2": "^3.749.0", 17 | "@aws-sdk/client-cloudwatch-logs": "^3.749.0", 18 | "@aws-sdk/client-eventbridge": "^3.749.0", 19 | "@aws-sdk/client-iam": "^3.749.0", 20 | "@aws-sdk/client-lambda": "^3.749.0", 21 | "@aws-sdk/client-sts": "^3.749.0", 22 | "@notation/core": "workspace:*", 23 | "@notation/std.iac": "workspace:*", 24 | "@types/aws-lambda": "^8.10.147", 25 | "zod": "^3.24.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/aws/src/event-bridge/aws-conversions.ts: -------------------------------------------------------------------------------- 1 | import { Schedule } from "./schedule"; 2 | 3 | export const toAwsScheduleExpression = (schedule: Schedule): string => { 4 | if (schedule.type === "rate") { 5 | return `rate(${schedule.rate} ${schedule.unit})`; 6 | } else if (schedule.type === "cron") { 7 | return `cron(${schedule.cronExpression})`; 8 | } else { 9 | const dateTime = schedule.dateTime; 10 | const humanBasedMonth = dateTime.getMonth() + 1; 11 | const monthString = 12 | humanBasedMonth >= 10 ? `${humanBasedMonth}` : `0${humanBasedMonth}`; 13 | const dayString = 14 | dateTime.getDate() >= 10 15 | ? `${dateTime.getDate()}` 16 | : `0${dateTime.getDate()}`; 17 | 18 | return `at(${dateTime.getFullYear()}-${monthString})-${dayString}-${dateTime.toLocaleTimeString()}`; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/aws.iac/src/utils/aws-clients.ts: -------------------------------------------------------------------------------- 1 | import { STSClient } from "@aws-sdk/client-sts"; 2 | import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs"; 3 | import { ApiGatewayV2Client } from "@aws-sdk/client-apigatewayv2"; 4 | import { LambdaClient } from "@aws-sdk/client-lambda"; 5 | import { IAMClient } from "@aws-sdk/client-iam"; 6 | import { region } from "src/config"; 7 | import { EventBridgeClient } from "@aws-sdk/client-eventbridge"; 8 | 9 | export const stsClient = new STSClient({ region }); 10 | export const cloudWatchLogsClient = new CloudWatchLogsClient({ region }); 11 | export const lambdaClient = new LambdaClient({ region }); 12 | export const apiGatewayClient = new ApiGatewayV2Client({ region }); 13 | export const iamClient = new IAMClient({ region }); 14 | 15 | export const eventBridgeClient = new EventBridgeClient({ region }); 16 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/src/plugins/function-runtime-plugin.ts: -------------------------------------------------------------------------------- 1 | import { Plugin } from "esbuild"; 2 | import { removeConfigExport } from "src/parsers/remove-config-export"; 3 | import { GetFile, fsGetFile, withFileCheck } from "src/utils/get-file"; 4 | 5 | type PluginOpts = { 6 | getFile?: GetFile; 7 | }; 8 | 9 | export function functionRuntimePlugin(opts: PluginOpts = {}): Plugin { 10 | return { 11 | name: "function-runtime", 12 | setup(build) { 13 | build.onLoad({ filter: /.\.fn*/ }, async (args) => { 14 | const getFile = withFileCheck(opts.getFile || fsGetFile); 15 | const fileContent = await getFile(args.path); 16 | const runtimeCode = removeConfigExport(fileContent); 17 | return { 18 | loader: "ts", 19 | contents: runtimeCode, 20 | }; 21 | }); 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/operations/operation.base.ts: -------------------------------------------------------------------------------- 1 | import { BaseResource } from "src/orchestrator/resource"; 2 | 3 | export const operation = ( 4 | action: string, 5 | operation: (opts: Opts) => Promise, 6 | ) => { 7 | return async ( 8 | opts: Opts & { dryRun?: boolean; quiet?: boolean }, 9 | ): Promise => { 10 | const { dryRun, quiet, ...opOpts } = opts; 11 | const message = `${action} ${opOpts.resource.id}`; 12 | 13 | if (dryRun) { 14 | console.log(`[Dry Run]: ${message}`); 15 | return {} as V; 16 | } 17 | 18 | try { 19 | const result = await operation(opOpts as unknown as Opts); 20 | if (!quiet) console.log(`[Success]: ${message}`); 21 | return result; 22 | } catch (err) { 23 | console.error(`[Error]: ${message}`); 24 | throw err; 25 | } 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /examples/lambda-external/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "lib": ["esnext"], 6 | "module": "esnext", 7 | "target": "esnext", 8 | 9 | "moduleResolution": "bundler", 10 | "noEmit": true, 11 | 12 | "composite": false, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "inlineSources": false, 17 | "isolatedModules": true, 18 | "noUnusedLocals": false, 19 | "noUnusedParameters": false, 20 | "preserveWatchOutput": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true, 23 | "strict": true, 24 | "strictNullChecks": true, 25 | 26 | "paths": { 27 | "runtime/*": ["./runtime/*"], 28 | "infra/*": ["./infra/*"] 29 | } 30 | }, 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/workflows/workflow.destroy.ts: -------------------------------------------------------------------------------- 1 | import { getResourceGraph } from "src/orchestrator/graph"; 2 | import { deleteResource } from "../operations"; 3 | import { State } from "../state"; 4 | import { refreshState } from "./workflow.refresh"; 5 | 6 | export async function destroyApp(entryPoint: string) { 7 | console.log(`Destroying ${entryPoint}\n`); 8 | 9 | const graph = await getResourceGraph(entryPoint); 10 | const state = new State(); 11 | 12 | await refreshState(entryPoint); 13 | 14 | for (const resource of graph.resources.reverse()) { 15 | const stateNode = await state.get(resource.id); 16 | if (!stateNode) { 17 | console.log( 18 | `[Skip]: Resource ${resource.type} ${resource.id} not found in state.`, 19 | ); 20 | continue; 21 | } 22 | resource.setOutput(stateNode.output); 23 | await deleteResource({ resource, state }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/create-notation/templates/starter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "lib": ["esnext"], 6 | "module": "esnext", 7 | "target": "esnext", 8 | 9 | "moduleResolution": "bundler", 10 | "noEmit": true, 11 | 12 | "composite": false, 13 | "declaration": true, 14 | "declarationMap": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "inlineSources": false, 17 | "isolatedModules": true, 18 | "noUnusedLocals": false, 19 | "noUnusedParameters": false, 20 | "preserveWatchOutput": true, 21 | "skipDefaultLibCheck": true, 22 | "skipLibCheck": true, 23 | "strict": true, 24 | "strictNullChecks": true, 25 | 26 | "paths": { 27 | "runtime/*": ["./runtime/*"], 28 | "infra/*": ["./infra/*"] 29 | } 30 | }, 31 | "exclude": ["node_modules"] 32 | } 33 | -------------------------------------------------------------------------------- /packages/dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@notation/dashboard", 3 | "version": "0.11.1", 4 | "type": "module", 5 | "main": "dist/server.js", 6 | "scripts": { 7 | "dev": "vite & tsup --watch", 8 | "build": "tsc && vite build && tsup", 9 | "preview": "vite preview", 10 | "typecheck": "tsc --noEmit" 11 | }, 12 | "files": [ 13 | "dist" 14 | ], 15 | "dependencies": { 16 | "@fastify/static": "^8.1.0", 17 | "chokidar": "^4.0.3", 18 | "fastify": "^5.2.1", 19 | "react": "^19.0.0", 20 | "react-dom": "^19.0.0" 21 | }, 22 | "devDependencies": { 23 | "@notation/core": "workspace:*", 24 | "@tailwindcss/typography": "^0.5.16", 25 | "@tailwindcss/vite": "^4.0.6", 26 | "@types/react": "^19.0.8", 27 | "@types/react-dom": "^19.0.3", 28 | "@vitejs/plugin-react": "^4.3.4", 29 | "tailwindcss": "^4.0.6", 30 | "typescript": "^5.7.3", 31 | "vite": "^6.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/dashboard/src/main.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @plugin '@tailwindcss/typography'; 4 | 5 | @theme { 6 | --breakpoint-3xl: 2000px; 7 | 8 | --color-transparent: transparent; 9 | --color-current: currentColor; 10 | 11 | --color-brand-500: #fe5537; 12 | --color-brand-700: #e8543a; 13 | --color-brand: #fe5537; 14 | 15 | --text-base: 0.825rem; 16 | } 17 | 18 | /* 19 | The default border color has changed to `currentColor` in Tailwind CSS v4, 20 | so we've added these compatibility styles to make sure everything still 21 | looks the same as it did with Tailwind CSS v3. 22 | 23 | If we ever want to remove these styles, we need to add an explicit border 24 | color utility to any element that depends on these defaults. 25 | */ 26 | @layer base { 27 | *, 28 | ::after, 29 | ::before, 30 | ::backdrop, 31 | ::file-selector-button { 32 | border-color: var(--color-gray-200, currentColor); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @notation/core 2 | 3 | ## 0.11.1 4 | 5 | ### Patch Changes 6 | 7 | - Hide large buffers from state file 8 | 9 | ## 0.11.0 10 | 11 | ## 0.10.0 12 | 13 | ### Minor Changes 14 | 15 | - Fix package versions 16 | 17 | ## 0.6.1 18 | 19 | ### Patch Changes 20 | 21 | - Remove log 22 | 23 | ## 0.6.0 24 | 25 | ### Minor Changes 26 | 27 | - 2a6fc59: Add optional JWT authorizer config to route resource 28 | 29 | ## 0.5.1 30 | 31 | ### Patch Changes 32 | 33 | - Fix updating std.zip resource 34 | 35 | ## 0.5.0 36 | 37 | ### Minor Changes 38 | 39 | - 5debdd1: Show deployed resource state in dashboard 40 | 41 | ## 0.4.1 42 | 43 | ### Patch Changes 44 | 45 | - Removed dev artifacts from dist 46 | 47 | ## 0.4.0 48 | 49 | ### Minor Changes 50 | 51 | - Stateful deployments 52 | 53 | ## 0.3.1 54 | 55 | ### Patch Changes 56 | 57 | - b75f89b: Clean up path method 58 | 59 | ## 0.3.0 60 | 61 | ### Minor Changes 62 | 63 | - Prepare for release 64 | -------------------------------------------------------------------------------- /packages/aws/src/event-bridge/schedule.ts: -------------------------------------------------------------------------------- 1 | export type RateUnit = "minute" | "minutes" | "hour" | "hours" | "day" | "days"; 2 | 3 | export type RateSchedule = { 4 | type: "rate"; 5 | rate: number; 6 | unit: RateUnit; 7 | }; 8 | 9 | export type CronSchedule = { 10 | type: "cron"; 11 | cronExpression: string; 12 | }; 13 | 14 | export type OneTimeSchedule = { 15 | type: "once"; 16 | dateTime: Date; 17 | }; 18 | 19 | export type Schedule = RateSchedule | CronSchedule | OneTimeSchedule; 20 | 21 | export const rate = (rate: number, unit: RateUnit): Schedule => { 22 | return { 23 | type: "rate", 24 | rate: rate, 25 | unit: unit, 26 | }; 27 | }; 28 | 29 | export const cron = (cronExpression: string): Schedule => { 30 | return { 31 | type: "cron", 32 | cronExpression: cronExpression, 33 | }; 34 | }; 35 | 36 | export const once = (dateTime: Date): Schedule => { 37 | return { 38 | type: "once", 39 | dateTime: dateTime, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/test/esbuild-test-utils.ts: -------------------------------------------------------------------------------- 1 | import esbuild, { BuildOptions, Plugin } from "esbuild"; 2 | 3 | const virtualFilePlugin = (input: any): Plugin => ({ 4 | name: "virtual-file", 5 | setup(build) { 6 | build.onResolve({ filter: /\..*/ }, (args) => { 7 | return { path: args.path, namespace: "stdin" }; 8 | }); 9 | build.onLoad({ filter: /\..*/, namespace: "stdin" }, (args) => { 10 | return { 11 | contents: input, 12 | loader: "ts", 13 | resolveDir: args.path, 14 | }; 15 | }); 16 | }, 17 | }); 18 | 19 | export function createBuilder( 20 | getBuildOptions: (input: string) => BuildOptions, 21 | ) { 22 | return async (input: string) => { 23 | const buildOptions = getBuildOptions(input); 24 | 25 | const result = await esbuild.build({ 26 | write: false, 27 | ...buildOptions, 28 | plugins: [...(buildOptions.plugins || []), virtualFilePlugin(input)], 29 | logLevel: "silent", 30 | }); 31 | 32 | return result.outputFiles![0].text; 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/operations/operation.delete.ts: -------------------------------------------------------------------------------- 1 | import { operation } from "./operation.base"; 2 | import { BaseResource } from "src/orchestrator/resource"; 3 | import { State } from "../state"; 4 | 5 | export const deleteResource = operation("Destroying", delete_); 6 | 7 | async function delete_(opts: { resource: BaseResource; state: State }) { 8 | const { resource, state } = opts; 9 | 10 | try { 11 | await resource.delete(resource.key, resource.toState(resource.output)); 12 | } catch (err: any) { 13 | // @todo: declare these in the resource provider 14 | if ( 15 | [ 16 | "NotFoundException", 17 | "ResourceNotFoundException", 18 | "NoSuchEntityException", 19 | ].includes(err.name) || 20 | ["ENOENT"].find((srt) => err.message.includes(srt)) 21 | ) { 22 | console.log( 23 | `Resource ${resource.type} ${resource.id} has already been deleted. \n→ Removing from state.\n`, 24 | ); 25 | } else { 26 | throw err; 27 | } 28 | } 29 | 30 | await state.delete(resource.id); 31 | } 32 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/src/parsers/remove-config-export.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function removeConfigExport(sourceText: string): string { 4 | const sourceFile = ts.createSourceFile( 5 | "file.ts", 6 | sourceText, 7 | ts.ScriptTarget.ES2015, 8 | true, 9 | ); 10 | 11 | const printer = ts.createPrinter(); 12 | const resultFile = ts.transform(sourceFile, [removeConfigExportTransformer]) 13 | .transformed[0]; 14 | 15 | const result = printer.printNode( 16 | ts.EmitHint.Unspecified, 17 | resultFile, 18 | sourceFile, 19 | ); 20 | 21 | return result; 22 | } 23 | 24 | function removeConfigExportTransformer(context: ts.TransformationContext) { 25 | return (node: ts.Node): ts.Node => { 26 | if ( 27 | ts.isVariableStatement(node) && 28 | node.declarationList.declarations.some( 29 | (decl) => decl.name.getText() === "config", 30 | ) 31 | ) { 32 | return ts.factory.createNotEmittedStatement(node); 33 | } 34 | return ts.visitEachChild( 35 | node, 36 | (child) => removeConfigExportTransformer(context)(child), 37 | context, 38 | ); 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/operations/operation.update.ts: -------------------------------------------------------------------------------- 1 | import { operation } from "./operation.base"; 2 | import { BaseResource } from "src/orchestrator/resource"; 3 | import { State } from "../state"; 4 | import { readResource } from "."; 5 | 6 | export const updateResource = operation("Updating", update); 7 | 8 | async function update(opts: { 9 | resource: BaseResource; 10 | state: State; 11 | patch: any; 12 | }): Promise { 13 | const { resource, state, patch } = opts; 14 | 15 | if (!resource.update) { 16 | throw new Error( 17 | `Update not implemented for ${resource.type} ${resource.id}`, 18 | ); 19 | } 20 | 21 | const params = await resource.getParams(); 22 | 23 | await resource.update( 24 | resource.key, 25 | patch, 26 | params, 27 | resource.toState(resource.output), 28 | ); 29 | resource.setOutput({ ...resource.key, ...params }); 30 | 31 | const result = await readResource({ resource, state, quiet: true }); 32 | resource.setOutput({ ...resource.output, ...result }); 33 | 34 | await state.update(resource.id, { 35 | lastOperation: "update", 36 | lastOperationAt: new Date().toISOString(), 37 | params: resource.toState(params), 38 | output: resource.toState(resource.output), 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/watch.ts: -------------------------------------------------------------------------------- 1 | import chokidar from "chokidar"; 2 | import { deployApp } from "@notation/core"; 3 | import { compile } from "./compile"; 4 | 5 | const dotFilesRe = /(^|[\/\\])\../; 6 | 7 | export async function watch(entryPoint: string) { 8 | await compile(entryPoint, true); 9 | 10 | const watcher = chokidar.watch("dist", { 11 | ignored: dotFilesRe, 12 | persistent: true, 13 | }); 14 | 15 | watcher.on("all", debounceDeploy); 16 | 17 | let isDeploying = false; 18 | let deployQueued = false; 19 | let timeoutId: NodeJS.Timeout; 20 | const debounceTime = 500; 21 | 22 | function debounceDeploy() { 23 | if (timeoutId) { 24 | clearTimeout(timeoutId); 25 | } 26 | timeoutId = setTimeout(() => { 27 | triggerDeploy(); 28 | }, debounceTime); 29 | } 30 | 31 | function triggerDeploy() { 32 | if (isDeploying) { 33 | deployQueued = true; 34 | return; 35 | } 36 | 37 | isDeploying = true; 38 | 39 | deployApp(entryPoint, false) 40 | .then(() => { 41 | isDeploying = false; 42 | if (deployQueued) { 43 | deployQueued = false; 44 | triggerDeploy(); 45 | } 46 | }) 47 | .catch((err) => { 48 | console.error(err); 49 | isDeploying = false; 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/pr.test.yaml: -------------------------------------------------------------------------------- 1 | name: "Integration Tests" 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | 7 | concurrency: 8 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 9 | cancel-in-progress: true 10 | 11 | jobs: 12 | integration-tests: 13 | runs-on: ubuntu-latest 14 | 15 | permissions: 16 | contents: "read" 17 | id-token: "write" 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - uses: pnpm/action-setup@v2 24 | with: 25 | version: 8 26 | 27 | - name: Get pnpm store directory 28 | shell: bash 29 | run: | 30 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 31 | 32 | - uses: actions/cache@v3 33 | name: Set up pnpm cache 34 | with: 35 | path: ${{ env.STORE_PATH }} 36 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 37 | restore-keys: | 38 | ${{ runner.os }}-pnpm-store- 39 | 40 | - name: Install 41 | run: pnpm install 42 | 43 | - name: Build 44 | run: pnpm build 45 | 46 | - name: Install local bin scripts 47 | run: pnpm install 48 | 49 | - name: Test 50 | run: pnpm test:once 51 | -------------------------------------------------------------------------------- /packages/aws/src/lambda.fn/handlers.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApiGatewayHandler, 3 | DynamoDbBatchHandler, 4 | DynamoDbStreamHandler, 5 | SqsBatchHandler, 6 | SqsHandler, 7 | EventBridgeHandler, 8 | JWTAuthorizedApiGatewayHandler, 9 | } from "src/shared/lambda.handler"; 10 | 11 | export const handle = { 12 | apiRequest: 13 | (handler: ApiGatewayHandler): ApiGatewayHandler => 14 | async (...args) => 15 | handler(...args), 16 | jwtAuthorizedApiRequest: 17 | ( 18 | handler: JWTAuthorizedApiGatewayHandler, 19 | ): JWTAuthorizedApiGatewayHandler => 20 | async (...args) => 21 | handler(...args), 22 | eventBridgeScheduledEvent: 23 | ( 24 | handler: EventBridgeHandler<"Scheduled Event", any>, 25 | ): EventBridgeHandler<"Scheduled Event", any> => 26 | async (...args) => 27 | handler(...args), 28 | dynamoDbStream: 29 | (handler: DynamoDbStreamHandler): DynamoDbStreamHandler => 30 | async (...args) => 31 | handler(...args), 32 | dynamoDbBatch: 33 | (handler: DynamoDbBatchHandler): DynamoDbBatchHandler => 34 | async (...args) => 35 | handler(...args), 36 | sqsEvent: 37 | (handler: SqsHandler): SqsHandler => 38 | async (...args) => 39 | handler(...args), 40 | sqsBatch: 41 | (handler: SqsBatchHandler): SqsBatchHandler => 42 | async (...args) => 43 | handler(...args), 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notation", 3 | "type": "module", 4 | "private": true, 5 | "packageManager": "pnpm@10.4.0", 6 | "scripts": { 7 | "build": "turbo run build", 8 | "dev": "turbo run dev --concurrency=20", 9 | "test": "vitest watch", 10 | "test:once": "vitest run", 11 | "format": "prettier --write .", 12 | "typecheck": "turbo run typecheck --continue", 13 | "changeset": "changeset", 14 | "version": "turbo run build && changeset version && pnpm install", 15 | "release": "changeset publish", 16 | "prepare": "husky" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/notation-dev/notation.git" 21 | }, 22 | "author": "Daniel Grant", 23 | "license": "Apache-2.0", 24 | "bugs": { 25 | "url": "https://github.com/notation-dev/notation/issues" 26 | }, 27 | "homepage": "https://github.com/notation-dev/notation#readme", 28 | "dependencies": { 29 | "@changesets/cli": "^2.27.12", 30 | "@notation/aws.iac": "workspace:*", 31 | "@notation/std.iac": "workspace:*", 32 | "@types/node": "^22.13.4", 33 | "execa": "^9.5.2", 34 | "glob": "^11.0.1", 35 | "husky": "^9.1.7", 36 | "prettier": "^3.5.1", 37 | "tsconfig": "workspace:*", 38 | "tsup": "^8.3.6", 39 | "turbo": "^2.4.2", 40 | "typescript": "^5.7.3", 41 | "vite-tsconfig-paths": "^5.1.4", 42 | "vitest": "^3.0.5" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/test/plugins/function-runtime-plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { stripIndent } from "common-tags"; 3 | import { functionRuntimePlugin } from "src/plugins/function-runtime-plugin"; 4 | import { createBuilder } from "test/esbuild-test-utils"; 5 | 6 | const buildRuntime = createBuilder((input) => ({ 7 | entryPoints: ["entry.fn.ts"], 8 | plugins: [functionRuntimePlugin({ getFile: () => input })], 9 | })); 10 | 11 | it("strips infra code", async () => { 12 | const input = stripIndent` 13 | import { LambdaConfig } from "@notation/aws/lambda"; 14 | import { handler } from "@notation/aws/api-gateway"; 15 | 16 | const num = await fetch("http://api.com/num").then((res) => res.json()); 17 | 18 | export const getNum = handler(() => num); 19 | export const getDoubleNum = handler(() => num * 2); 20 | 21 | export const config: LambdaConfig = { 22 | memory: 64, 23 | timeout: 5, 24 | environment: "node:16", 25 | }; 26 | `; 27 | 28 | const expected = stripIndent` 29 | import { handler } from "@notation/aws/api-gateway"; 30 | const num = await fetch("http://api.com/num").then((res) => res.json()); 31 | export const getNum = handler(() => num); 32 | export const getDoubleNum = handler(() => num * 2); 33 | `; 34 | 35 | const output = await buildRuntime(input); 36 | 37 | expect(output).toContain(expected); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/core/src/orchestrator/resource-group.ts: -------------------------------------------------------------------------------- 1 | import { resources, resourceGroups, getNextResourceGroupCount } from "./state"; 2 | import { BaseResource } from "./resource"; 3 | 4 | export type ResourceGroupOptions = { 5 | dependencies?: Record; 6 | [key: string]: any; 7 | }; 8 | 9 | export abstract class ResourceGroup { 10 | type: string; 11 | id: number; 12 | dependencies: Record; 13 | config: Record; 14 | resources: BaseResource[]; 15 | 16 | constructor(type: string, opts: ResourceGroupOptions) { 17 | const { dependencies, ...config } = opts; 18 | this.type = type; 19 | this.id = getNextResourceGroupCount(); 20 | this.dependencies = dependencies || {}; 21 | this.config = config || {}; 22 | this.resources = []; 23 | resourceGroups.push(this); 24 | return this; 25 | } 26 | 27 | add(resource: T) { 28 | if (resources.includes(resource)) { 29 | throw new Error(`Resource ${resource.type} has already been registered.`); 30 | } 31 | resource.groupId = this.id; 32 | resource.groupType = this.type; 33 | resources.push(resource); 34 | this.resources.push(resource); 35 | return resource; 36 | } 37 | 38 | findResource BaseResource>( 39 | ResourceClass: T, 40 | ) { 41 | return this.resources.find((r) => r instanceof ResourceClass) as 42 | | InstanceType 43 | | undefined; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/std.iac/src/resources/fs/file.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import { getSourceSha256 } from "src/utils/hash"; 3 | import * as z from "zod"; 4 | import * as fs from "node:fs/promises"; 5 | 6 | export type FileSchema = { 7 | Key: { filePath: string }; 8 | CreateParams: { filePath: string }; 9 | UpdateParams: { filePath: string }; 10 | ReadResult: void; 11 | }; 12 | 13 | const fileResource = resource({ 14 | type: "std/fs/File", 15 | }); 16 | 17 | export const fileSchema = fileResource.defineSchema({ 18 | filePath: { 19 | valueType: z.string(), 20 | propertyType: "param", 21 | presence: "required", 22 | primaryKey: true, 23 | }, 24 | sourceSha256: { 25 | valueType: z.string(), 26 | propertyType: "param", 27 | presence: "required", 28 | }, 29 | file: { 30 | valueType: z.instanceof(Buffer), 31 | propertyType: "computed", 32 | presence: "required", 33 | hidden: true, 34 | }, 35 | } as const); 36 | 37 | export const File = fileSchema.defineOperations({ 38 | setIntrinsicConfig: async ({ config }) => { 39 | const sourceSha256 = await getSourceSha256(config.filePath!); 40 | return { sourceSha256 }; 41 | }, 42 | read: async (config) => { 43 | const file = await fs.readFile(config.filePath); 44 | return { ...config, file }; 45 | }, 46 | create: async () => {}, 47 | update: async () => {}, 48 | delete: async () => {}, 49 | }); 50 | 51 | export type FileInstance = InstanceType; 52 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/workflows/workflow.refresh.ts: -------------------------------------------------------------------------------- 1 | import { getResourceGraph } from "src/orchestrator/graph"; 2 | import { deleteResource } from "../operations/operation.delete"; 3 | import { State } from "../state"; 4 | import { Resource } from "src/orchestrator/resource"; 5 | 6 | /** 7 | * @description Destroy resources that are in state but not in the orchestration graph 8 | */ 9 | export async function refreshState( 10 | entryPoint: string, 11 | dryRun = false, 12 | ): Promise { 13 | const log = (message: string) => 14 | dryRun ? console.log(`[Dry Run]: ${message}`) : console.log(message); 15 | 16 | log(`Refreshing ${entryPoint} state\n`); 17 | 18 | const graph = await getResourceGraph(entryPoint); 19 | const state = new State(); 20 | 21 | for (const stateNode of (await state.values()).reverse()) { 22 | let resource = graph.resources.find((r) => r.id === stateNode.id); 23 | 24 | if (!resource) { 25 | const { moduleName, serviceName, resourceName } = stateNode.meta; 26 | const provider = await import(moduleName); 27 | const Resource = provider[serviceName][resourceName]; 28 | 29 | resource = new Resource({ 30 | id: stateNode.id, 31 | meta: stateNode.meta, 32 | }) as Resource; 33 | 34 | resource.setOutput(stateNode.output); 35 | 36 | if (!dryRun) { 37 | await deleteResource({ resource, state, dryRun }); 38 | } 39 | 40 | log(`Deleted ${resource.type} ${resource.id}`); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/dashboard/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { useRemoteState } from "./hooks/remote-state"; 2 | import { Header } from "./blocks/header"; 3 | import { ResourceList } from "./blocks/resource-list"; 4 | import { useEffect, useState } from "react"; 5 | import { BaseResource } from "@notation/core"; 6 | import { Resource } from "./blocks/resource"; 7 | 8 | function App() { 9 | const state = useRemoteState(); 10 | const resources = Object.values(state); 11 | const [activeResource, setActiveResource] = useState(); 12 | 13 | useEffect(() => { 14 | if (resources.length > 0 && !activeResource) { 15 | // todo: remove hack 16 | const resource = 17 | resources.find((r) => r.meta.serviceName === "apiGateway") || 18 | resources[0]; 19 | setActiveResource(resource); 20 | } 21 | }, [resources, activeResource]); 22 | 23 | return ( 24 |
25 |
26 |
27 |
28 | setActiveResource(resource)} 32 | /> 33 |
34 |
35 | {activeResource && } 36 |
37 |
38 |
39 | ); 40 | } 41 | 42 | export default App; 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | .pnpm-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # Dependency directories 28 | node_modules/ 29 | jspm_packages/ 30 | 31 | # TypeScript cache 32 | *.tsbuildinfo 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional eslint cache 38 | .eslintcache 39 | 40 | # Output of 'npm pack' 41 | *.tgz 42 | 43 | # turbo 44 | .turbo 45 | 46 | # dotenv environment variable files 47 | .env 48 | .env.development.local 49 | .env.test.local 50 | .env.production.local 51 | .env.local 52 | 53 | # compiled output 54 | dist 55 | tmp 56 | /out-tsc 57 | tsconfig.tsbuildinfo 58 | 59 | # dependencies 60 | node_modules 61 | 62 | # IDEs and editors 63 | /.idea 64 | .project 65 | .classpath 66 | .c9/ 67 | *.launch 68 | .settings/ 69 | *.sublime-workspace 70 | 71 | # IDE - VSCode 72 | .vscode/* 73 | !.vscode/settings.json 74 | !.vscode/tasks.json 75 | !.vscode/launch.json 76 | !.vscode/extensions.json 77 | 78 | # misc 79 | /connect.lock 80 | /coverage 81 | npm-debug.log 82 | 83 | # System Files 84 | .DS_Store 85 | Thumbs.db 86 | 87 | # notation 88 | .notation -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { program } from "commander"; 3 | import { compile } from "./compile"; 4 | import { deploy } from "./deploy"; 5 | import { destroy } from "./destroy"; 6 | import { visualise } from "./visualise"; 7 | import { watch } from "./watch"; 8 | import { startDashboardServer } from "@notation/dashboard"; 9 | 10 | program 11 | .command("compile") 12 | .argument("", "entryPoint") 13 | .description("Compile Notation App") 14 | .action(async (entryPoint) => { 15 | await compile(entryPoint); 16 | }); 17 | 18 | program 19 | .command("dashboard") 20 | .description("Start Notation Dashboard") 21 | .action(async () => { 22 | await startDashboardServer(); 23 | }); 24 | 25 | program 26 | .command("deploy") 27 | .argument("", "entryPoint") 28 | .description("Deploy Notation App") 29 | .action(async (entryPoint) => { 30 | await deploy(entryPoint); 31 | }); 32 | 33 | program 34 | .command("destroy") 35 | .argument("", "entryPoint") 36 | .description("Destroy Notation App") 37 | .action(async (entryPoint) => { 38 | await destroy(entryPoint); 39 | }); 40 | 41 | program 42 | .command("viz") 43 | .argument("", "entryPoint") 44 | .description("Visualise Notation App") 45 | .action(async (entryPoint) => { 46 | await visualise(entryPoint); 47 | }); 48 | 49 | program 50 | .command("watch") 51 | .argument("", "entryPoint") 52 | .description("Watch Notation App") 53 | .action(async (entryPoint) => { 54 | await watch(entryPoint); 55 | }); 56 | 57 | program.parse(process.argv); 58 | -------------------------------------------------------------------------------- /packages/dashboard/server/server.ts: -------------------------------------------------------------------------------- 1 | import Fastify from "fastify"; 2 | import fastifyStatic from "@fastify/static"; 3 | import chokidar from "chokidar"; 4 | import fs from "fs/promises"; 5 | import path from "path"; 6 | import { fileURLToPath } from "url"; 7 | import { dirname } from "path"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = dirname(__filename); 11 | 12 | const fastify = Fastify({}); 13 | 14 | const statePath = path.join(process.cwd(), "./.notation/state.json"); 15 | 16 | fastify.register(fastifyStatic, { 17 | root: path.join(__dirname, "./"), 18 | prefix: "/", 19 | }); 20 | 21 | fastify.get("/state", (request, reply) => { 22 | reply.raw.setHeader("Content-Type", "text/event-stream"); 23 | reply.raw.setHeader("Cache-Control", "no-cache"); 24 | reply.raw.setHeader("Connection", "keep-alive"); 25 | 26 | const sendState = async () => { 27 | try { 28 | const state = await fs.readFile(statePath, "utf8"); 29 | reply.raw.write(`data: ${JSON.stringify(JSON.parse(state))}\n\n`); 30 | } catch (error) { 31 | console.error("Error reading state.json:", error); 32 | } 33 | }; 34 | 35 | const watcher = chokidar.watch(statePath); 36 | 37 | watcher.on("change", sendState); 38 | 39 | sendState(); 40 | 41 | request.raw.on("close", () => { 42 | watcher.close(); 43 | }); 44 | 45 | reply.hijack(); 46 | }); 47 | 48 | export const startDashboardServer = async (port: number = 6682) => { 49 | try { 50 | await fastify.listen({ port }); 51 | console.log("\nNotation dashboard is running on:\n\n"); 52 | console.log(`➜ http://localhost:${port}`); 53 | } catch (err) { 54 | console.error(err); 55 | process.exit(1); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /packages/aws/test/api-gateway.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, beforeEach } from "vitest"; 2 | import { reset } from "@notation/core"; 3 | import { apiGateway } from "@notation/aws.iac"; 4 | import { NO_AUTH, api, router } from "src/api-gateway"; 5 | import { route } from "src/api-gateway/route"; 6 | import { lambda } from "src/lambda"; 7 | 8 | beforeEach(() => { 9 | reset(); 10 | }); 11 | 12 | test("route resource group idempotency snapshot", () => { 13 | const apiResourceGroup = api({ name: "api" }); 14 | const fnResourceGroup = lambda({ 15 | code: { 16 | type: "file", 17 | path: "src/fns/handler.fn.js", 18 | }, 19 | handler: "handler.fn.js", 20 | }); 21 | 22 | route(apiResourceGroup, "GET", "/hello", NO_AUTH, fnResourceGroup as any); 23 | const fnResourceGroupSnapshot = JSON.stringify(fnResourceGroup); 24 | route(apiResourceGroup, "POST", "/hello", NO_AUTH, fnResourceGroup as any); 25 | const fnResourceGroupSnapshot2 = JSON.stringify(fnResourceGroup); 26 | 27 | expect(fnResourceGroupSnapshot).toEqual(fnResourceGroupSnapshot2); 28 | }); 29 | 30 | test("router provides methods for each HTTP verb", () => { 31 | const apiResourceGroup = api({ name: "api" }); 32 | const apiRouter = router(apiResourceGroup); 33 | const handler = lambda({ 34 | code: { 35 | type: "file", 36 | path: "src/fns/handler.fn.js", 37 | }, 38 | handler: "handler.fn.js", 39 | }); 40 | 41 | for (const method of ["get", "post", "put", "delete", "patch"] as const) { 42 | const routeGroup = apiRouter[method]("/hello", handler as any); 43 | const route = routeGroup.findResource(apiGateway.Route)!; 44 | expect(route.config.RouteKey).toEqual(`${method.toUpperCase()} /hello`); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /packages/core/src/visualiser/chart.ts: -------------------------------------------------------------------------------- 1 | import pako from "pako"; 2 | import { fromUint8Array } from "js-base64"; 3 | import { ResourceGroup } from "../orchestrator/resource-group"; 4 | import { BaseResource } from "../orchestrator/resource"; 5 | 6 | export const createMermaidFlowChart = ( 7 | resourceGroups: ResourceGroup[], 8 | resources: BaseResource[], 9 | ): string => { 10 | let mermaidString = "flowchart TD\n"; 11 | let connectionsString = ""; 12 | 13 | resourceGroups.forEach((group) => { 14 | mermaidString += ` subgraph ${group.type}_${group.id}\n`; 15 | group.resources.forEach((resource) => { 16 | mermaidString += ` ${resource.type}_${resource.id}(${resource.type})\n`; 17 | }); 18 | mermaidString += ` end\n`; 19 | 20 | group.resources.forEach((resource) => { 21 | (Object.values(resource.dependencies) as BaseResource[]).forEach( 22 | (dep) => { 23 | if (!dep) return; 24 | const depResource = resources.find((r) => r.id === dep.id); 25 | if (depResource) { 26 | connectionsString += ` ${resource.type}_${resource.id} --> ${depResource.type}_${dep.id}\n`; 27 | } 28 | }, 29 | ); 30 | }); 31 | }); 32 | 33 | return `${mermaidString}\n${connectionsString}`; 34 | }; 35 | 36 | export function createMermaidLiveUrl(mermaidCode: string) { 37 | const state = { 38 | code: mermaidCode, 39 | mermaid: JSON.stringify({ theme: "default" }, undefined, 2), 40 | autoSync: true, 41 | updateDiagram: true, 42 | }; 43 | const json = JSON.stringify(state); 44 | const data = new TextEncoder().encode(json); 45 | const compressed = pako.deflate(data, { level: 9 }); 46 | const string = fromUint8Array(compressed, true); 47 | return `https://mermaid.live/view#pako:${string}`; 48 | } 49 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @notation/esbuild-plugins 2 | 3 | ## 0.11.1 4 | 5 | ### Patch Changes 6 | 7 | - Hide large buffers from state file 8 | - Updated dependencies 9 | - @notation/core@0.11.1 10 | 11 | ## 0.11.0 12 | 13 | ### Patch Changes 14 | 15 | - @notation/core@0.11.0 16 | 17 | ## 0.10.0 18 | 19 | ### Minor Changes 20 | 21 | - Fix package versions 22 | 23 | ### Patch Changes 24 | 25 | - Updated dependencies 26 | - @notation/core@0.10.0 27 | 28 | ## 0.6.1 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies 33 | - @notation/core@0.6.1 34 | 35 | ## 0.6.0 36 | 37 | ### Minor Changes 38 | 39 | - Support externally managed lambda modules 40 | 41 | ## 0.5.0 42 | 43 | ### Minor Changes 44 | 45 | - 2a6fc59: Add optional JWT authorizer config to route resource 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [2a6fc59] 50 | - @notation/core@0.6.0 51 | 52 | ## 0.4.2 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies 57 | - @notation/core@0.5.1 58 | 59 | ## 0.4.1 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [5debdd1] 64 | - @notation/core@0.5.0 65 | 66 | ## 0.4.0 67 | 68 | ### Minor Changes 69 | 70 | - Load infra in runtime modules 71 | 72 | ### Patch Changes 73 | 74 | - Updated dependencies 75 | - @notation/core@0.4.1 76 | 77 | ## 0.3.2 78 | 79 | ### Patch Changes 80 | 81 | - Updated dependencies 82 | - @notation/core@0.4.0 83 | 84 | ## 0.3.1 85 | 86 | ### Patch Changes 87 | 88 | - Updated dependencies [b75f89b] 89 | - @notation/core@0.3.1 90 | 91 | ## 0.3.0 92 | 93 | ### Minor Changes 94 | 95 | - Prepare for release 96 | 97 | ### Patch Changes 98 | 99 | - Updated dependencies 100 | - @notation/core@0.3.0 101 | -------------------------------------------------------------------------------- /packages/std.iac/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @notation/std.iac 2 | 3 | ## 0.11.1 4 | 5 | ### Patch Changes 6 | 7 | - Hide large buffers from state file 8 | - Updated dependencies 9 | - @notation/core@0.11.1 10 | 11 | ## 0.11.0 12 | 13 | ### Patch Changes 14 | 15 | - @notation/core@0.11.0 16 | 17 | ## 0.10.0 18 | 19 | ### Minor Changes 20 | 21 | - Fix package versions 22 | 23 | ### Patch Changes 24 | 25 | - Updated dependencies 26 | - @notation/core@0.10.0 27 | 28 | ## 0.6.1 29 | 30 | ### Patch Changes 31 | 32 | - Updated dependencies 33 | - @notation/core@0.6.1 34 | 35 | ## 0.6.0 36 | 37 | ### Minor Changes 38 | 39 | - Support externally managed lambda modules 40 | 41 | ## 0.5.0 42 | 43 | ### Minor Changes 44 | 45 | - 2a6fc59: Add optional JWT authorizer config to route resource 46 | 47 | ### Patch Changes 48 | 49 | - Updated dependencies [2a6fc59] 50 | - @notation/core@0.6.0 51 | 52 | ## 0.4.3 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies 57 | - @notation/core@0.5.1 58 | 59 | ## 0.4.2 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [5debdd1] 64 | - @notation/core@0.5.0 65 | 66 | ## 0.4.1 67 | 68 | ### Patch Changes 69 | 70 | - Removed dev artifacts from dist 71 | - Updated dependencies 72 | - @notation/core@0.4.1 73 | 74 | ## 0.4.0 75 | 76 | ### Minor Changes 77 | 78 | - Stateful deployments 79 | 80 | ### Patch Changes 81 | 82 | - Updated dependencies 83 | - @notation/core@0.4.0 84 | 85 | ## 0.3.1 86 | 87 | ### Patch Changes 88 | 89 | - Updated dependencies [b75f89b] 90 | - @notation/core@0.3.1 91 | 92 | ## 0.3.0 93 | 94 | ### Minor Changes 95 | 96 | - Prepare for release 97 | 98 | ### Patch Changes 99 | 100 | - Updated dependencies 101 | - @notation/core@0.3.0 102 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/src/plugins/function-infra-plugin.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { Plugin } from "esbuild"; 3 | import { parseFnModule } from "src/parsers/parse-fn-module"; 4 | import { removeUnsafeReferences } from "src/parsers/remove-unsafe-references"; 5 | import { GetFile, fsGetFile, withFileCheck } from "src/utils/get-file"; 6 | import { filePaths } from "@notation/core"; 7 | 8 | type PluginOpts = { 9 | getFile?: GetFile; 10 | }; 11 | 12 | export function functionInfraPlugin(opts: PluginOpts = {}): Plugin { 13 | return { 14 | name: "function-infra", 15 | setup(build) { 16 | build.onLoad({ filter: /.\.fn*/ }, async (args) => { 17 | const getFile = withFileCheck(opts.getFile || fsGetFile); 18 | const fileContent = await getFile(args.path); 19 | const fileName = path.relative(process.cwd(), args.path); 20 | const outFileName = filePaths.dist.runtime.index(fileName); 21 | const safeFnModule = removeUnsafeReferences(fileContent); 22 | const { config, exports } = parseFnModule(fileContent); 23 | 24 | const reservedNames = ["preload", "config"]; 25 | const [platform, service] = (config.service as string).split("/"); 26 | 27 | let infraCode = `import { ${service} } from "@notation/${platform}/${service}";\n`; 28 | infraCode = infraCode.concat(`\n${safeFnModule}\n`); 29 | 30 | for (const handlerName of exports) { 31 | if (reservedNames.includes(handlerName)) continue; 32 | infraCode = infraCode.concat( 33 | `export const ${handlerName} = ${service}({ code: { type: "file", path: "${outFileName}" }, handler: "${handlerName}", ...config });\n`, 34 | ); 35 | } 36 | 37 | return { 38 | contents: infraCode, 39 | loader: "ts", 40 | }; 41 | }); 42 | }, 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/state.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra/esm"; 2 | import { Resource } from ".."; 3 | 4 | export type StateNode = { 5 | id: string; 6 | groupId: number; 7 | groupType: string; 8 | meta: Resource["meta"]; 9 | config: {}; 10 | params: {}; 11 | output: {}; 12 | lastOperation: "drift" | "create" | "update" | "delete"; 13 | lastOperationAt: string; 14 | }; 15 | 16 | export class State { 17 | state: Record; 18 | constructor() { 19 | this.state = {}; 20 | } 21 | async get(id: string) { 22 | this.state = await readState(); 23 | return this.state[id]; 24 | } 25 | async has(id: string) { 26 | this.state = await readState(); 27 | return !!this.state[id]; 28 | } 29 | async update(id: string, patch: Partial) { 30 | this.state = await readState(); 31 | this.state[id] = { 32 | ...this.state[id], 33 | ...patch, 34 | }; 35 | await writeState(this.state); 36 | } 37 | async delete(id: string) { 38 | this.state = await readState(); 39 | delete this.state[id]; 40 | await writeState(this.state); 41 | } 42 | async values() { 43 | this.state = await readState(); 44 | return Object.values(this.state); 45 | } 46 | } 47 | 48 | async function readState(): Promise> { 49 | const filePath = "./.notation/state.json"; 50 | 51 | if (await fsExtra.pathExists(filePath)) { 52 | return fsExtra.readJSON(filePath); 53 | } else { 54 | await fsExtra.ensureFile(filePath); 55 | await fsExtra.writeJSON(filePath, {}); 56 | return {}; 57 | } 58 | } 59 | 60 | async function writeState(state: Record) { 61 | await fsExtra.ensureDir("./.notation"); 62 | await fsExtra.ensureFile("./.notation/state.json"); 63 | await fsExtra.writeJSON("./.notation/state.json", state, { spaces: 2 }); 64 | } 65 | -------------------------------------------------------------------------------- /packages/dashboard/src/blocks/resource-list.tsx: -------------------------------------------------------------------------------- 1 | import { BaseResource } from "@notation/core"; 2 | 3 | export function ResourceList(props: { 4 | resources: BaseResource[]; 5 | activeResourceId?: string; 6 | onClick: (resource: BaseResource) => void; 7 | }) { 8 | return ( 9 |
10 |
    11 | {aggregateResources(props.resources).map((resourceGroup) => ( 12 |
      16 |
    • 17 | {resourceGroup[0].groupType} #{resourceGroup[0].groupId} 18 |
    • 19 |
        20 | {resourceGroup.map((resource) => ( 21 |
      • props.onClick(resource)} 27 | > 28 | {resource.id} 29 |
      • 30 | ))} 31 |
      32 |
    33 | ))} 34 |
35 |
36 | ); 37 | } 38 | 39 | function aggregateResources(resources: BaseResource[]) { 40 | const groupedResources: Record = {}; 41 | resources.forEach((resource) => { 42 | const key = `${resource.groupId}`; 43 | if (!groupedResources[key]) { 44 | groupedResources[key] = []; 45 | } 46 | groupedResources[key].push(resource); 47 | }); 48 | return Object.values(groupedResources).sort((a, b) => 49 | a[0].groupType.localeCompare(b[0].groupType), 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /packages/aws/src/shared/lambda.handler.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Context, 3 | APIGatewayProxyEventV2, 4 | APIGatewayProxyResultV2, 5 | EventBridgeEvent, 6 | DynamoDBStreamEvent, 7 | DynamoDBBatchResponse, 8 | SQSEvent, 9 | SQSBatchResponse, 10 | APIGatewayProxyEventV2WithJWTAuthorizer, 11 | } from "aws-lambda"; 12 | 13 | export type ApiGatewayHandler = ( 14 | event: APIGatewayProxyEventV2, 15 | context: Context, 16 | ) => APIGatewayProxyResultV2 | Promise; 17 | 18 | export type TypedClaimsOverride = { 19 | requestContext: { 20 | authorizer: { 21 | jwt: { 22 | claims: T; 23 | scopes: string[]; 24 | }; 25 | }; 26 | }; 27 | }; 28 | 29 | export type APIGatewayJWTProxyEventV2 = Omit< 30 | APIGatewayProxyEventV2WithJWTAuthorizer, 31 | "requestContext" 32 | > & 33 | TypedClaimsOverride; 34 | 35 | export type JWTAuthorizedApiGatewayHandler = ( 36 | event: APIGatewayJWTProxyEventV2, 37 | context: Context, 38 | ) => APIGatewayProxyResultV2 | Promise; 39 | 40 | export type EventBridgeHandler = ( 41 | event: EventBridgeEvent, 42 | context: Context, 43 | ) => void | Promise; 44 | 45 | export type EventBridgeScheduleHandler = EventBridgeHandler< 46 | "Scheduled Event", 47 | any 48 | >; 49 | export type DynamoDbStreamHandler = ( 50 | event: DynamoDBStreamEvent, 51 | context: Context, 52 | ) => void | Promise; 53 | 54 | export type DynamoDbBatchHandler = ( 55 | event: DynamoDBBatchResponse, 56 | context: Context, 57 | ) => void | Promise; 58 | 59 | export type SqsHandler = ( 60 | event: SQSEvent, 61 | context: Context, 62 | ) => void | Promise; 63 | 64 | export type SqsBatchHandler = ( 65 | event: SQSBatchResponse, 66 | context: Context, 67 | ) => void | Promise; 68 | -------------------------------------------------------------------------------- /packages/dashboard/src/blocks/resource.tsx: -------------------------------------------------------------------------------- 1 | import { BaseResource } from "@notation/core"; 2 | 3 | export function Resource(props: { resource: BaseResource }) { 4 | return ( 5 |
6 |
7 |
8 | {getHeading(props.resource)} 9 |
10 |
11 | {getSubHeading(props.resource)} 12 |
13 |
14 |
15 | 24 | {getMeta(props.resource)} 25 |
26 |
27 | ); 28 | } 29 | 30 | const getHeading = (resource: BaseResource) => { 31 | return `${resource.id}`; 32 | }; 33 | 34 | const getSubHeading = (resource: BaseResource) => { 35 | return `${resource.meta.serviceName}.${resource.meta.resourceName}`; 36 | }; 37 | 38 | const getConsoleUrl = (resource: BaseResource & { output: any }) => { 39 | switch (resource.meta.serviceName) { 40 | case "apiGateway": 41 | return `https://us-west-2.console.aws.amazon.com/apigateway/main/develop/routes?api=${resource.output.ApiId}®ion=us-west-2`; 42 | default: 43 | return ""; 44 | } 45 | }; 46 | 47 | const getMeta = (resource: BaseResource & { output: any }) => { 48 | if (resource.meta.serviceName === "apiGateway") { 49 | return ( 50 | 59 | ); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /packages/aws/src/event-bridge/event-bridge-schedule.ts: -------------------------------------------------------------------------------- 1 | import { EventBridgeHandler } from "src/shared/lambda.handler"; 2 | import { Schedule } from "./schedule"; 3 | import * as aws from "@notation/aws.iac"; 4 | import { toAwsScheduleExpression } from "./aws-conversions"; 5 | 6 | export const schedule = (config: { 7 | name: string; 8 | schedule: Schedule; 9 | handler: 10 | | EventBridgeHandler<"Scheduled Event", any> 11 | // todo: narrow to lambda group 12 | | aws.AwsResourceGroup; 13 | }): aws.AwsResourceGroup => { 14 | const eventBridgeScheduleGroup = new aws.AwsResourceGroup( 15 | "aws/eventBridge/schedule", 16 | config, 17 | ); 18 | 19 | const lambdaGroup = 20 | config.handler instanceof aws.AwsResourceGroup 21 | ? config.handler 22 | : // at compile time, runtime module becomes infra resource group 23 | (config.handler as any as aws.AwsResourceGroup); 24 | 25 | const lambdaResource = lambdaGroup.findResource(aws.lambda.LambdaFunction)!; 26 | 27 | const eventBridgeRule = new aws.eventBridge.EventBridgeRule({ 28 | id: `${config.name}-eventbridge-rule`, 29 | config: { 30 | Name: config.name, 31 | ScheduleExpression: toAwsScheduleExpression(config.schedule), 32 | EventBusName: "default", 33 | }, 34 | dependencies: { 35 | lambda: lambdaResource, 36 | }, 37 | }); 38 | 39 | eventBridgeScheduleGroup.add(eventBridgeRule); 40 | 41 | const permission = lambdaGroup.findResource( 42 | aws.eventBridge.LambdaEventBridgeRulePermission, 43 | ); 44 | 45 | if (!permission) { 46 | lambdaGroup.add( 47 | new aws.eventBridge.LambdaEventBridgeRulePermission({ 48 | id: `${config.name}-eventbridge-permission`, 49 | dependencies: { 50 | lambda: lambdaResource, 51 | eventBridgeRule: eventBridgeRule, 52 | }, 53 | }), 54 | ); 55 | } 56 | 57 | return eventBridgeScheduleGroup; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/aws/test/lambda.fn.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, beforeEach } from "vitest"; 2 | import { reset } from "@notation/core"; 3 | import { handle, json } from "src/lambda.fn"; 4 | import { APIGatewayJWTProxyEventV2 } from "src/shared"; 5 | import { Context } from "aws-lambda"; 6 | 7 | beforeEach(() => { 8 | reset(); 9 | }); 10 | 11 | test("handlers wrap user-provided handlers", async () => { 12 | const fn = async (): Promise => ({ body: "{}" }); 13 | const result = await handle.apiRequest(fn)({} as any, {} as any); 14 | expect(result).toEqual({ body: "{}" }); 15 | }); 16 | 17 | test("jwtAuthorizedApiRequest passes through JWT token as expected", async () => { 18 | type ClaimsFields = { 19 | iss: string; 20 | }; 21 | 22 | const jwtTokenHandler = handle.jwtAuthorizedApiRequest( 23 | (event: APIGatewayJWTProxyEventV2, context: Context) => { 24 | return { 25 | body: JSON.stringify({ 26 | name: event.requestContext.authorizer.jwt.claims.iss, 27 | }), 28 | }; 29 | }, 30 | ); 31 | 32 | const input: APIGatewayJWTProxyEventV2 = { 33 | requestContext: { 34 | authorizer: { 35 | jwt: { 36 | claims: { 37 | iss: "John Doe", 38 | }, 39 | scopes: [], 40 | }, 41 | }, 42 | }, 43 | headers: {}, 44 | routeKey: "", 45 | rawPath: "", 46 | isBase64Encoded: false, 47 | rawQueryString: "", 48 | version: "1", 49 | }; 50 | 51 | const result = await jwtTokenHandler(input, {} as Context); 52 | 53 | expect(result).toEqual({ body: JSON.stringify({ name: "John Doe" }) }); 54 | }); 55 | 56 | test("json returns a JSON string and a 200 status code", () => { 57 | const payload = { message: "Hello, world!" }; 58 | const response = json(payload); 59 | expect(response.statusCode).toEqual(200); 60 | expect(response.body).toEqual(JSON.stringify(payload)); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/lambda/lambda-role-policy-attachment.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import * as sdk from "@aws-sdk/client-iam"; 3 | import * as z from "zod"; 4 | import { iamClient } from "src/utils/aws-clients"; 5 | import { AwsSchema } from "src/utils/types"; 6 | import { LambdaIamRoleInstance } from "."; 7 | 8 | export type LambdaRolePolicyAttachmentSchema = AwsSchema<{ 9 | Key: sdk.DetachRolePolicyRequest; 10 | CreateParams: sdk.AttachRolePolicyRequest; 11 | }>; 12 | 13 | export type LambdaRolePolicyAttachmentDependencies = { 14 | role: LambdaIamRoleInstance; 15 | }; 16 | 17 | const lambdaRolePolicyAttachment = resource({ 18 | type: "aws/lambda/LambdaRolePolicyAttachment", 19 | }); 20 | 21 | const lambdaRolePolicyAttachmentSchema = 22 | lambdaRolePolicyAttachment.defineSchema({ 23 | RoleName: { 24 | valueType: z.string(), 25 | propertyType: "param", 26 | presence: "required", 27 | primaryKey: true, 28 | }, 29 | PolicyArn: { 30 | valueType: z.string(), 31 | propertyType: "param", 32 | presence: "required", 33 | secondaryKey: true, 34 | }, 35 | } as const); 36 | 37 | export const LambdaRolePolicyAttachment = lambdaRolePolicyAttachmentSchema 38 | .defineOperations({ 39 | create: async (params) => { 40 | const command = new sdk.AttachRolePolicyCommand(params); 41 | await iamClient.send(command); 42 | }, 43 | delete: async (key) => { 44 | const command = new sdk.DetachRolePolicyCommand(key); 45 | await iamClient.send(command); 46 | }, 47 | }) 48 | .requireDependencies() 49 | .setIntrinsicConfig(({ deps }) => ({ 50 | RoleName: deps.role.output.RoleName, 51 | PolicyArn: 52 | "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 53 | })); 54 | 55 | export type LambdaRolePolicyAttachmentInstance = InstanceType< 56 | typeof LambdaRolePolicyAttachment 57 | >; 58 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/operations/operation.read.ts: -------------------------------------------------------------------------------- 1 | import { operation } from "./operation.base"; 2 | import { BaseResource } from "src/orchestrator/resource"; 3 | import { State } from "../state"; 4 | 5 | export const readResource = operation("Reading", read); 6 | 7 | async function read(opts: { resource: BaseResource; state: State }) { 8 | const { resource, state } = opts; 9 | 10 | let backoff = 1000; 11 | 12 | if (!resource.read) { 13 | const params = await resource.getParams(); 14 | const stateNode = await state.get(resource.id); 15 | if (!stateNode) return params; 16 | return { ...stateNode.output, ...params }; 17 | } 18 | 19 | async function getSettledReadResult() { 20 | if (!resource.read) return {}; 21 | const readResult = await resource.read(resource.key); 22 | 23 | const needsRetry = resource.retryReadOnCondition?.some((condition) => { 24 | if (!condition) return false; 25 | 26 | const { key, value, reason } = condition; 27 | const msg = `[Info]: ${reason}`; 28 | 29 | if (value && readResult[key] !== value) { 30 | console.log(msg); 31 | return true; 32 | } 33 | 34 | if (!readResult[key]) { 35 | console.log(msg); 36 | return true; 37 | } 38 | 39 | return false; 40 | }); 41 | 42 | if (needsRetry) { 43 | await new Promise((resolve) => setTimeout(resolve, backoff)); 44 | backoff *= 1.2; 45 | return getSettledReadResult(); 46 | } 47 | 48 | return readResult; 49 | } 50 | 51 | try { 52 | const params = await resource.getParams(); 53 | const result = await getSettledReadResult(); 54 | return { ...params, ...result }; 55 | } catch (err: any) { 56 | // todo: normalise not found errors within resource class 57 | // add logic for interpreting error in resource 58 | // if (err.name === "NoSuchEntityException") { 59 | // return null; 60 | // } 61 | console.log( 62 | `[Error]: Reading remote resource ${resource.type} ${resource.id}`, 63 | ); 64 | throw err; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/aws/src/api-gateway/router.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApiGatewayHandler, 3 | JWTAuthorizedApiGatewayHandler, 4 | } from "src/shared"; 5 | import * as aws from "@notation/aws.iac"; 6 | import { route } from "./route"; 7 | import { api } from "./api"; 8 | import { AuthorizerConfig, JWTAuthorizerConfig, NO_AUTH } from "./auth"; 9 | 10 | export const router = (apiGroup: ReturnType) => { 11 | const createRouteCallback = 12 | (method: string) => 13 | (path: `/${string}`, handler: ApiGatewayHandler | aws.AwsResourceGroup) => { 14 | return route(apiGroup, method, path, NO_AUTH, handler); 15 | }; 16 | 17 | return { 18 | get: createRouteCallback("GET"), 19 | post: createRouteCallback("POST"), 20 | put: createRouteCallback("PUT"), 21 | patch: createRouteCallback("PATCH"), 22 | delete: createRouteCallback("DELETE"), 23 | withJWTAuthorizer: (auth: JWTAuthorizerConfig) => { 24 | const authorizer = new AuthorizedRouteBuilder(apiGroup); 25 | return authorizer.withJWTAuthorizer(auth); 26 | }, 27 | }; 28 | }; 29 | 30 | class AuthorizedRouteBuilder { 31 | auth: AuthorizerConfig = NO_AUTH; 32 | apiGroup: ReturnType; 33 | 34 | constructor(apiGroup: ReturnType) { 35 | this.apiGroup = apiGroup; 36 | } 37 | 38 | private createJWTAuthorizedRouteCallback = 39 | (method: string, authorizer: JWTAuthorizerConfig) => 40 | ( 41 | path: `/${string}`, 42 | handler: JWTAuthorizedApiGatewayHandler, 43 | ) => { 44 | return route(this.apiGroup, method, path, authorizer, handler); 45 | }; 46 | 47 | withJWTAuthorizer(authorizer: JWTAuthorizerConfig) { 48 | return { 49 | get: this.createJWTAuthorizedRouteCallback("GET", authorizer), 50 | post: this.createJWTAuthorizedRouteCallback( 51 | "POST", 52 | authorizer, 53 | ), 54 | put: this.createJWTAuthorizedRouteCallback("PUT", authorizer), 55 | patch: this.createJWTAuthorizedRouteCallback( 56 | "PATCH", 57 | authorizer, 58 | ), 59 | delete: this.createJWTAuthorizedRouteCallback( 60 | "DELETE", 61 | authorizer, 62 | ), 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/std.iac/src/resources/fs/zip.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | import * as fs from "node:fs/promises"; 3 | import { resource } from "@notation/core"; 4 | import { zip } from "src/utils/zip"; 5 | import { getSourceSha256 } from "src/utils/hash"; 6 | 7 | export type ZipSchema = { 8 | Key: { sourceFilePath: string }; 9 | CreateParams: { sourceFilePath: string }; 10 | UpdateParams: { sourceFilePath: string }; 11 | ReadResult: void; 12 | }; 13 | 14 | const zipResource = resource({ 15 | type: "std/fs/Zip", 16 | }); 17 | 18 | export const zipSchema = zipResource.defineSchema({ 19 | sourceFilePath: { 20 | valueType: z.string(), 21 | propertyType: "param", 22 | presence: "required", 23 | primaryKey: true, 24 | }, 25 | filePath: { 26 | valueType: z.string(), 27 | propertyType: "param", 28 | presence: "required", 29 | secondaryKey: true, 30 | }, 31 | sourceSha256: { 32 | valueType: z.string(), 33 | propertyType: "param", 34 | presence: "required", 35 | }, 36 | file: { 37 | valueType: z.instanceof(Buffer), 38 | propertyType: "computed", 39 | presence: "required", 40 | hidden: true, 41 | }, 42 | } as const); 43 | 44 | export const Zip = zipSchema.defineOperations({ 45 | setIntrinsicConfig: async ({ config }) => { 46 | const sourceSha256 = await getSourceSha256(config.sourceFilePath!); 47 | const filePath = `${config.sourceFilePath}.zip`; 48 | return { sourceSha256, filePath }; 49 | }, 50 | read: async (params) => { 51 | try { 52 | const file = await fs.readFile(params.filePath); 53 | return { ...params, file }; 54 | } catch (error: any) { 55 | if (error.code !== "ENOENT") throw error; 56 | await zip.package(params.sourceFilePath, params.filePath); 57 | const file = await fs.readFile(params.filePath); 58 | return { ...params, file }; 59 | } 60 | }, 61 | create: async (params) => { 62 | await zip.package(params.sourceFilePath, params.filePath); 63 | }, 64 | update: async (config) => { 65 | await fs.unlink(config.filePath); 66 | await zip.package(config.sourceFilePath, config.filePath); 67 | }, 68 | delete: async (config) => { 69 | await fs.unlink(config.filePath); 70 | }, 71 | }); 72 | 73 | export type ZipFileInstance = InstanceType; 74 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/operations/operation.create.ts: -------------------------------------------------------------------------------- 1 | import { operation } from "./operation.base"; 2 | import { BaseResource } from "src/orchestrator/resource"; 3 | import { State } from "../state"; 4 | import { readResource } from "."; 5 | 6 | export const createResource = operation("Creating", create); 7 | 8 | async function create( 9 | opts: { resource: BaseResource; state: State }, 10 | backoff = 1000, 11 | ) { 12 | const { resource, state } = opts; 13 | 14 | try { 15 | const params = await resource.getParams(); 16 | const maybeComputedPrimaryKey = await resource.create(params); 17 | 18 | resource.setOutput(params); 19 | 20 | if (maybeComputedPrimaryKey) { 21 | resource.setOutput({ ...maybeComputedPrimaryKey, ...resource.output }); 22 | } 23 | 24 | const readResult = await readResource({ resource, state, quiet: true }); 25 | 26 | resource.setOutput({ ...resource.output, ...readResult }); 27 | 28 | await state.update(resource.id, { 29 | id: resource.id, 30 | groupId: resource.groupId, 31 | groupType: resource.groupType, 32 | meta: resource.meta, 33 | lastOperation: "create", 34 | lastOperationAt: new Date().toISOString(), 35 | config: resource.config, 36 | params: resource.toState(params), 37 | output: resource.toState(resource.output), 38 | }); 39 | } catch (err: any) { 40 | const retryCondition = 41 | resource.retryLaterOnError && 42 | resource.retryLaterOnError.find( 43 | (retry) => retry.name === err.name && retry.message === err.message, 44 | ); 45 | 46 | if (retryCondition) { 47 | console.log(`[Retry]: Creating ${resource.type} ${resource.id}`); 48 | console.log(`[Reason]: ${retryCondition.reason}`); 49 | await new Promise((resolve) => setTimeout(resolve, backoff)); 50 | backoff *= 1.5; 51 | await create(opts, backoff); 52 | } 53 | // else if (err.name === "ConflictException") { 54 | // // todo: provide some means to requisition the resource 55 | // console.log( 56 | // `[Info]: Resource ${resource.type} ${resource.id} already exists but isn't owned by Notation.`, 57 | // ); 58 | // throw err; 59 | // } 60 | else { 61 | console.log(`[Error]: ${err}`); 62 | throw err; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/core/test/provisioner/operation.create.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi, afterEach } from "vitest"; 2 | import { createResource } from "src/provisioner/operations/operation.create"; 3 | import { State, StateNode } from "src/provisioner/state"; 4 | import { 5 | TestResourceSchema, 6 | testResourceConfig, 7 | testOperations, 8 | testResourceOutput, 9 | } from "test/orchestrator/resource.doubles"; 10 | import { reset } from "src/orchestrator/state"; 11 | 12 | const stateMock = { 13 | get: vi.fn((id: number) => Promise.resolve({}) as any as StateNode), 14 | update: vi.fn((id: number, patch: any) => Promise.resolve()), 15 | delete: vi.fn((id: number) => Promise.resolve()), 16 | values: vi.fn(() => [] as StateNode[]), 17 | } as any as State; 18 | 19 | afterEach(() => { 20 | reset(); 21 | Object.values(stateMock).forEach((fn) => fn.mockClear()); 22 | }); 23 | 24 | describe("resource creation", () => { 25 | const readResult = { ...testResourceOutput, volatileComputed: "123" }; 26 | const createMock = vi.fn(() => Promise.resolve({ primaryKey: "" })); 27 | const readMock = vi.fn(() => Promise.resolve(readResult)); 28 | 29 | const TestResource = TestResourceSchema.defineOperations({ 30 | ...testOperations, 31 | create: createMock, 32 | read: readMock, 33 | }); 34 | 35 | const testResource = new TestResource({ 36 | id: "test-resource", 37 | config: testResourceConfig, 38 | }); 39 | 40 | it("passes computed input to resource.create", async () => { 41 | await createResource({ 42 | resource: testResource, 43 | state: stateMock, 44 | quiet: true, 45 | }); 46 | const params = await testResource.getParams(); 47 | expect(createMock.mock.calls[0]).toEqual([params]); 48 | }); 49 | 50 | it("updates the state", async () => { 51 | await createResource({ 52 | resource: testResource, 53 | state: stateMock, 54 | quiet: true, 55 | }); 56 | expect(stateMock.update).toHaveBeenCalledOnce(); 57 | }); 58 | 59 | it("sets the resource output with the read result", async () => { 60 | await createResource({ 61 | resource: testResource, 62 | state: stateMock, 63 | quiet: true, 64 | }); 65 | expect(testResource.output).not.toEqual(testResourceOutput); 66 | expect(testResource.output).toEqual(readResult); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /packages/aws.iac/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @notation/aws.iac 2 | 3 | ## 0.11.1 4 | 5 | ### Patch Changes 6 | 7 | - Hide large buffers from state file 8 | - Updated dependencies 9 | - @notation/core@0.11.1 10 | - @notation/std.iac@0.11.1 11 | 12 | ## 0.11.0 13 | 14 | ### Minor Changes 15 | 16 | - 5dcb935: Support alternative runtimes 17 | 18 | ### Patch Changes 19 | 20 | - @notation/core@0.11.0 21 | - @notation/std.iac@0.11.0 22 | 23 | ## 0.10.0 24 | 25 | ### Minor Changes 26 | 27 | - Fix package versions 28 | 29 | ### Patch Changes 30 | 31 | - Updated dependencies 32 | - @notation/core@0.10.0 33 | - @notation/std.iac@0.10.0 34 | 35 | ## 0.6.1 36 | 37 | ### Patch Changes 38 | 39 | - Updated dependencies 40 | - @notation/core@0.6.1 41 | - @notation/std.iac@0.6.1 42 | 43 | ## 0.6.0 44 | 45 | ### Minor Changes 46 | 47 | - Support externally managed lambda modules 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies 52 | - @notation/std.iac@0.6.0 53 | 54 | ## 0.5.0 55 | 56 | ### Minor Changes 57 | 58 | - 2a6fc59: Add optional JWT authorizer config to route resource 59 | 60 | ### Patch Changes 61 | 62 | - Updated dependencies [2a6fc59] 63 | - @notation/std.iac@0.5.0 64 | - @notation/core@0.6.0 65 | 66 | ## 0.4.3 67 | 68 | ### Patch Changes 69 | 70 | - Fix updating std.zip resource 71 | - Updated dependencies 72 | - @notation/core@0.5.1 73 | - @notation/std.iac@0.4.3 74 | 75 | ## 0.4.2 76 | 77 | ### Patch Changes 78 | 79 | - Updated dependencies [5debdd1] 80 | - @notation/core@0.5.0 81 | - @notation/std.iac@0.4.2 82 | 83 | ## 0.4.1 84 | 85 | ### Patch Changes 86 | 87 | - Removed dev artifacts from dist 88 | - Updated dependencies 89 | - @notation/core@0.4.1 90 | - @notation/std.iac@0.4.1 91 | 92 | ## 0.4.0 93 | 94 | ### Minor Changes 95 | 96 | - Stateful deployments 97 | 98 | ### Patch Changes 99 | 100 | - Updated dependencies 101 | - @notation/core@0.4.0 102 | - @notation/std.iac@0.4.0 103 | 104 | ## 0.3.1 105 | 106 | ### Patch Changes 107 | 108 | - Updated dependencies [b75f89b] 109 | - @notation/core@0.3.1 110 | - @notation/std.iac@0.3.1 111 | 112 | ## 0.3.0 113 | 114 | ### Minor Changes 115 | 116 | - Prepare for release 117 | 118 | ### Patch Changes 119 | 120 | - Updated dependencies 121 | - @notation/core@0.3.0 122 | - @notation/std.iac@0.3.0 123 | -------------------------------------------------------------------------------- /packages/create-notation/src/scaffold.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import fse from "fs-extra"; 3 | import { execSync } from "node:child_process"; 4 | import whichPmRuns from "which-pm-runs"; 5 | 6 | const dirname = path.dirname(new URL(import.meta.url).pathname); 7 | const packageManager = whichPmRuns()?.name || "npm"; 8 | const packageManagerInstallCommand = getPmInstallCommand(packageManager); 9 | 10 | export async function scaffoldApp(appName: string) { 11 | const cwd = process.cwd(); 12 | const appDir = path.join(cwd, appName); 13 | 14 | await fse.ensureDir(appDir); 15 | 16 | const children = await fse.readdir(appDir); 17 | 18 | if (children.length) { 19 | console.error( 20 | `Directory ${path.relative(process.cwd(), appDir)} is not empty`, 21 | ); 22 | return; 23 | } 24 | 25 | console.log(`\nCreating app from template...`); 26 | 27 | const templateDir = path.join(dirname, "../templates/starter"); 28 | 29 | await fse.copy(templateDir, appDir); 30 | 31 | const packageJsonPath = path.join(appDir, "package.json"); 32 | const packageJsonContent = await fse.readFile(packageJsonPath, "utf8"); 33 | const packageJson = JSON.parse(packageJsonContent); 34 | 35 | packageJson.name = appName; 36 | const updatedPackageJson = JSON.stringify(packageJson, null, 2); 37 | 38 | await fse.writeFile(packageJsonPath, updatedPackageJson, { 39 | encoding: "utf8", 40 | }); 41 | 42 | console.log(`Installing dependencies with ${packageManager}...`); 43 | 44 | execSync(`${packageManagerInstallCommand} @notation/cli @notation/aws`, { 45 | cwd: appDir, 46 | stdio: "inherit", 47 | }); 48 | 49 | console.log("\nApp ready! To get started run:\n"); 50 | console.log(`➜ cd ${appName}`); 51 | console.log(`➜ ${getPmRunCommand(packageManager)} viz`); 52 | console.log(`➜ ${getPmRunCommand(packageManager)} deploy`); 53 | } 54 | 55 | function getPmInstallCommand(pm: string) { 56 | switch (pm) { 57 | case "yarn": 58 | return "yarn add"; 59 | case "pnpm": 60 | return "pnpm add"; 61 | case "bun": 62 | return "bun add"; 63 | default: 64 | return "npm install"; 65 | } 66 | } 67 | 68 | function getPmRunCommand(pm: string) { 69 | switch (pm) { 70 | case "yarn": 71 | return "yarn"; 72 | case "pnpm": 73 | return "pnpm"; 74 | case "bun": 75 | return "bun"; 76 | default: 77 | return "npm run"; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/lambda/lambda-log-group.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import * as sdk from "@aws-sdk/client-cloudwatch-logs"; 3 | import * as z from "zod"; 4 | import { cloudWatchLogsClient } from "src/utils/aws-clients"; 5 | import { AwsSchema } from "src/utils/types"; 6 | import { LambdaFunctionInstance } from "../lambda"; 7 | 8 | export type LambdaLogGroupSchema = AwsSchema<{ 9 | Key: sdk.DeleteLogGroupRequest; 10 | CreateParams: sdk.CreateLogGroupRequest & sdk.PutRetentionPolicyRequest; 11 | }>; 12 | 13 | export type LambdaLogGroupDependencies = { lambda: LambdaFunctionInstance }; 14 | 15 | const lambdaLogGroup = resource({ 16 | type: "aws/lambda/LambdaLogGroup", 17 | }); 18 | 19 | const lambdaLogGroupSchema = lambdaLogGroup.defineSchema({ 20 | logGroupName: { 21 | valueType: z.string(), 22 | propertyType: "param", 23 | presence: "required", 24 | primaryKey: true, 25 | }, 26 | kmsKeyId: { 27 | valueType: z.string(), 28 | propertyType: "param", 29 | presence: "optional", 30 | }, 31 | tags: { 32 | valueType: z.record(z.string()), 33 | propertyType: "param", 34 | presence: "optional", 35 | }, 36 | retentionInDays: { 37 | valueType: z.number(), 38 | propertyType: "param", 39 | presence: "required", 40 | }, 41 | } as const); 42 | 43 | export const LambdaLogGroup = lambdaLogGroupSchema 44 | .defineOperations({ 45 | create: async (params) => { 46 | const command = new sdk.CreateLogGroupCommand(params); 47 | await cloudWatchLogsClient.send(command); 48 | const retentionCommand = new sdk.PutRetentionPolicyCommand({ 49 | logGroupName: params.logGroupName, 50 | retentionInDays: params.retentionInDays, 51 | }); 52 | await cloudWatchLogsClient.send(retentionCommand); 53 | }, 54 | update: async (key, params) => { 55 | const command = new sdk.PutRetentionPolicyCommand({ ...key, ...params }); 56 | await cloudWatchLogsClient.send(command); 57 | }, 58 | delete: async (key) => { 59 | const command = new sdk.DeleteLogGroupCommand(key); 60 | await cloudWatchLogsClient.send(command); 61 | }, 62 | }) 63 | .requireDependencies() 64 | .setIntrinsicConfig(({ deps }) => ({ 65 | logGroupName: `/aws/lambda/${deps.lambda.output.FunctionName}`, 66 | })); 67 | 68 | export type LambdaLogGroupInstance = InstanceType; 69 | -------------------------------------------------------------------------------- /packages/cli/src/compile.ts: -------------------------------------------------------------------------------- 1 | import chokidar from "chokidar"; 2 | import { glob } from "glob"; 3 | import { log } from "console"; 4 | import esbuild from "esbuild"; 5 | import { 6 | functionInfraPlugin, 7 | functionRuntimePlugin, 8 | } from "@notation/esbuild-plugins"; 9 | import { filePaths } from "@notation/core"; 10 | 11 | export async function compile(entryPoint: string, watch: boolean = false) { 12 | log(`${watch ? "Watching" : "Compiling"} infrastructure`, entryPoint); 13 | await compileInfra(entryPoint, watch); 14 | 15 | log(`${watch ? "Watching" : "Compiling"} functions`); 16 | 17 | // @todo: fnEntryPoints could be an output of compileInfra 18 | const fnEntryPoints = await glob("runtime/**/*.fn.ts"); 19 | let disposeFnCompiler = await compileFns(fnEntryPoints, watch); 20 | 21 | if (!watch) return; 22 | 23 | chokidar 24 | .watch("**/*.fn.ts", { 25 | ignored: /node_modules/, 26 | persistent: true, 27 | }) 28 | .on("all", async () => { 29 | if (disposeFnCompiler) disposeFnCompiler(); 30 | const fnEntryPoints = await glob("runtime/**/*.fn.ts"); 31 | disposeFnCompiler = await compileFns(fnEntryPoints, watch); 32 | }); 33 | } 34 | 35 | export async function compileInfra(entryPoint: string, watch: boolean = false) { 36 | const context = await esbuild.context({ 37 | entryPoints: [entryPoint], 38 | plugins: [functionInfraPlugin()], 39 | outdir: "dist", 40 | outbase: ".", 41 | outExtension: { ".js": ".mjs" }, 42 | bundle: true, 43 | format: "esm", 44 | platform: "node", 45 | treeShaking: true, 46 | packages: "external", 47 | }); 48 | 49 | if (watch) { 50 | await context.watch(); 51 | } else { 52 | await context.rebuild(); 53 | context.dispose(); 54 | } 55 | } 56 | 57 | export async function compileFns( 58 | entryPoints: string[], 59 | watch: boolean = false, 60 | ) { 61 | for (const entryPoint of entryPoints) { 62 | const context = await esbuild.context({ 63 | entryPoints: [entryPoint], 64 | plugins: [functionRuntimePlugin()], 65 | outfile: filePaths.dist.runtime.index(entryPoint), 66 | outExtension: { ".js": ".mjs" }, 67 | bundle: true, 68 | format: "esm", 69 | platform: "node", 70 | treeShaking: true, 71 | }); 72 | 73 | if (watch) { 74 | await context.watch(); 75 | return () => context.dispose(); 76 | } else { 77 | await context.rebuild(); 78 | context.dispose(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/aws/src/lambda/lambda.ts: -------------------------------------------------------------------------------- 1 | import * as aws from "@notation/aws.iac"; 2 | import * as std from "@notation/std.iac"; 3 | import crypto from "crypto"; 4 | import path from "path"; 5 | 6 | type LambdaConfig = { 7 | id?: string; 8 | handler: string; 9 | code: { 10 | type: "file" | "zip"; 11 | path: string; 12 | }; 13 | runtime?: aws.lambda.LambdaFunctionConfig["Runtime"]; 14 | }; 15 | 16 | export const lambda = (config: LambdaConfig): aws.AwsResourceGroup => { 17 | const functionGroup = new aws.AwsResourceGroup("Lambda", { config }); 18 | const filePath = config.code.path; 19 | 20 | let lambdaId = config.id; 21 | 22 | if (!lambdaId) { 23 | const filePathHash = crypto 24 | .createHash("BLAKE2s256") 25 | .update(filePath) 26 | .digest("hex") 27 | .slice(0, 8); 28 | 29 | lambdaId = `${config.handler}-${filePathHash}`; 30 | } 31 | 32 | let zipFile: std.fs.ZipFileInstance | std.fs.FileInstance; 33 | 34 | if (config.code.type === "file") { 35 | zipFile = functionGroup.add( 36 | new std.fs.Zip({ 37 | id: `${lambdaId}-zip`, 38 | config: { sourceFilePath: filePath }, 39 | }), 40 | ); 41 | } else { 42 | zipFile = functionGroup.add( 43 | new std.fs.File({ 44 | id: `${lambdaId}-zip`, 45 | config: { filePath }, 46 | }), 47 | ); 48 | } 49 | 50 | const role = functionGroup.add( 51 | new aws.lambda.LambdaIamRole({ 52 | id: `${lambdaId}-role`, 53 | config: { 54 | RoleName: `${lambdaId}-role`, 55 | }, 56 | }), 57 | ); 58 | 59 | functionGroup.add( 60 | new aws.lambda.LambdaRolePolicyAttachment({ 61 | id: `${lambdaId}-policy`, 62 | dependencies: { role }, 63 | }), 64 | ); 65 | 66 | const fileName = path.parse(filePath).name; 67 | const runtime = config.runtime || "nodejs22.x"; 68 | 69 | const lambdaResource = functionGroup.add( 70 | new aws.lambda.LambdaFunction({ 71 | id: lambdaId, 72 | config: { 73 | FunctionName: lambdaId, 74 | Handler: `${fileName}.${config.handler}`, 75 | Runtime: runtime, 76 | // todo: make this configurable and remove it as a default 77 | ReservedConcurrentExecutions: 1, 78 | }, 79 | dependencies: { 80 | role, 81 | zipFile, 82 | }, 83 | }), 84 | ); 85 | 86 | functionGroup.add( 87 | new aws.lambda.LambdaLogGroup({ 88 | id: `${lambdaId}-log-group`, 89 | config: { retentionInDays: 30 }, 90 | dependencies: { lambda: lambdaResource }, 91 | }), 92 | ); 93 | 94 | return functionGroup; 95 | }; 96 | -------------------------------------------------------------------------------- /packages/core/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export type IfAllPropertiesOptional = T extends Partial 2 | ? Partial extends T 3 | ? Y 4 | : N 5 | : N; 6 | 7 | /** 8 | * @typeParam K - The record key 9 | * @typeParam T - The record value type 10 | * @description If T is Partial, return { [key in K]?: T }, else return { [key in K]: T } 11 | */ 12 | export type OptionalIfAllPropertiesOptional< 13 | K extends string, 14 | T, 15 | > = IfAllPropertiesOptional; 16 | 17 | /** 18 | * Extracts the keys from type `T` that are required. 19 | * A key is considered required if it is present in `Required`. 20 | * 21 | * @typeParam T - The type to extract required keys from. 22 | * 23 | * @returns A union of the keys of `T` that are required. 24 | */ 25 | export type RequiredKeys = { 26 | [K in keyof T]-?: T[K] extends Required[K] ? K : never; 27 | }[keyof T]; 28 | 29 | /** 30 | * Creates a tuple type consisting of each required key of type `T` as a separate element. 31 | * This type maps over each key in `T`, including it in the tuple if it is required. 32 | * If `T` has no required keys, an empty tuple type is returned. 33 | * 34 | * @typeParam T - The type to create a tuple of required keys from. 35 | * 36 | * @returns A tuple type with each required key of `T` as a separate element, or an empty tuple if `T` has no required keys. 37 | */ 38 | export type RequiredKeysTuple = keyof T extends infer D 39 | ? D extends keyof T 40 | ? T[D] extends Required[D] 41 | ? [D, ...RequiredKeysTuple>] 42 | : RequiredKeysTuple> 43 | : never 44 | : []; 45 | 46 | /** 47 | * Provides a fallback value if the first type is `undefined`. 48 | * If the first type `T` is `undefined`, the fallback type `U` is used instead. 49 | * 50 | * @typeParam T - The first type. 51 | * @typeParam U - The fallback type. 52 | */ 53 | export type Fallback = T extends undefined ? U : T; 54 | 55 | /** 56 | * Provides a fallback if `T` is assignable to `C`. 57 | */ 58 | export type FallbackIf = C extends T ? U : T; 59 | 60 | /** 61 | * A utility type that prevents inference of a generic type parameter. 62 | * This is useful when you want to preserve the exact type passed as an argument. 63 | * 64 | * @typeParam T - The type to prevent inference for. 65 | * @returns T. 66 | * 67 | * @example 68 | * ```typescript 69 | * function foo(arg: NoInfer): void { 70 | * // ... 71 | * } 72 | * 73 | * const value: string = "Hello"; 74 | * foo(value); // Argument type is preserved as string 75 | * ``` 76 | */ 77 | export type NoInfer = [T][T extends any ? 0 : never]; 78 | -------------------------------------------------------------------------------- /packages/core/test/orchestrator/resource.doubles.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "src"; 2 | import { z } from "zod"; 3 | 4 | export type TestSchema = { 5 | Key: { 6 | primaryKey: string; 7 | optionalSecondaryKey: number; 8 | }; 9 | CreateParams: { 10 | optionalSecondaryKey: number; 11 | requiredParam: string; 12 | hiddenParam: string; 13 | intrinsicParam: boolean; 14 | }; 15 | UpdateParams: { 16 | optionalSecondaryKey: number; 17 | requiredParam: string; 18 | hiddenParam: string; 19 | intrinsicParam: boolean; 20 | }; 21 | ReadResult: { 22 | primaryKey: string; 23 | optionalSecondaryKey: number; 24 | requiredParam: string; 25 | volatileComputed: string; 26 | intrinsicParam: boolean; 27 | }; 28 | }; 29 | 30 | export const testSchema = { 31 | primaryKey: { 32 | presence: "required", 33 | propertyType: "computed", 34 | valueType: z.string(), 35 | primaryKey: true, 36 | }, 37 | optionalSecondaryKey: { 38 | presence: "optional", 39 | propertyType: "param", 40 | valueType: z.number(), 41 | secondaryKey: true, 42 | immutable: true, 43 | }, 44 | requiredParam: { 45 | presence: "required", 46 | propertyType: "param", 47 | valueType: z.string(), 48 | }, 49 | volatileComputed: { 50 | presence: "required", 51 | propertyType: "computed", 52 | valueType: z.string(), 53 | volatile: true, 54 | }, 55 | hiddenParam: { 56 | presence: "required", 57 | propertyType: "param", 58 | valueType: z.string(), 59 | hidden: true, 60 | }, 61 | intrinsicParam: { 62 | presence: "required", 63 | propertyType: "param", 64 | valueType: z.boolean(), 65 | }, 66 | } as const; 67 | 68 | export const testOperations = { 69 | async create() { 70 | return testResourceOutput; 71 | }, 72 | async delete() {}, 73 | async update() {}, 74 | async read() { 75 | return { primaryKey: "", optionalSecondaryKey: "", requiredParam: "" }; 76 | }, 77 | setIntrinsicConfig() { 78 | return { intrinsicParam: true }; 79 | }, 80 | }; 81 | 82 | export const TestResourceSchema = resource({ 83 | type: "provider/service/resource", 84 | }).defineSchema(testSchema); 85 | 86 | export const TestResource = TestResourceSchema.defineOperations(testOperations); 87 | 88 | export const TestResource2 = resource({ 89 | type: "provider/service/resource-2", 90 | }) 91 | .defineSchema(testSchema) 92 | .defineOperations(testOperations); 93 | 94 | export const testResourceOutput = { 95 | primaryKey: "0", 96 | requiredParam: "name", 97 | hiddenParam: "hidden", 98 | volatileComputed: "", 99 | intrinsicParam: true, 100 | }; 101 | 102 | export const testResourceConfig = { 103 | requiredParam: "name1", 104 | hiddenParam: "", 105 | }; 106 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/api-gateway/auth.ts: -------------------------------------------------------------------------------- 1 | import * as sdk from "@aws-sdk/client-apigatewayv2"; 2 | import { AwsSchema } from "src/utils/types"; 3 | import { resource } from "@notation/core"; 4 | import z from "zod"; 5 | import { apiGatewayClient } from "src/utils/aws-clients"; 6 | import { ApiInstance } from "./api"; 7 | 8 | type AuthorizerSdkSchema = AwsSchema<{ 9 | Key: sdk.GetAuthorizerRequest; 10 | CreateParams: sdk.CreateAuthorizerRequest; 11 | UpdateParams: sdk.UpdateAuthorizerRequest; 12 | ReadResult: sdk.GetAuthorizerResponse; 13 | }>; 14 | 15 | type AuthorizerDependencies = { 16 | api: ApiInstance; 17 | }; 18 | 19 | const authorizer = resource({ 20 | type: "aws/apiGateway/Authorizer", 21 | }); 22 | 23 | const apiSchema = authorizer.defineSchema({ 24 | ApiId: { 25 | valueType: z.string(), 26 | presence: "required", 27 | propertyType: "param", 28 | secondaryKey: true, 29 | }, 30 | AuthorizerId: { 31 | valueType: z.string(), 32 | propertyType: "computed", 33 | presence: "required", 34 | primaryKey: true, 35 | }, 36 | Name: { 37 | valueType: z.string(), 38 | propertyType: "param", 39 | presence: "required", 40 | }, 41 | AuthorizerType: { 42 | valueType: z.enum([sdk.AuthorizerType.JWT, sdk.AuthorizerType.REQUEST]), 43 | propertyType: "param", 44 | presence: "required", 45 | }, 46 | IdentitySource: { 47 | valueType: z.array(z.string()), 48 | presence: "required", 49 | propertyType: "param", 50 | }, 51 | JwtConfiguration: { 52 | valueType: z 53 | .object({ 54 | Audience: z.array(z.string()), 55 | Issuer: z.string(), 56 | }) 57 | .optional(), 58 | propertyType: "param", 59 | presence: "optional", 60 | }, 61 | } as const); 62 | 63 | export const RouteAuth = apiSchema 64 | .defineOperations({ 65 | create: async (params) => { 66 | const command = new sdk.CreateAuthorizerCommand(params); 67 | const result = await apiGatewayClient.send(command); 68 | 69 | return { 70 | AuthorizerId: result.AuthorizerId!, 71 | }; 72 | }, 73 | read: async (key) => { 74 | const command = new sdk.GetAuthorizerCommand(key); 75 | const result = await apiGatewayClient.send(command); 76 | 77 | return result; 78 | }, 79 | update: async (key, patch, params) => { 80 | const command = new sdk.UpdateAuthorizerCommand({ ...key, ...params }); 81 | await apiGatewayClient.send(command); 82 | }, 83 | delete: async (params) => { 84 | const command = new sdk.DeleteAuthorizerCommand(params); 85 | await apiGatewayClient.send(command); 86 | }, 87 | }) 88 | .requireDependencies() 89 | .setIntrinsicConfig(({ deps }) => ({ 90 | ApiId: deps.api.output.ApiId, 91 | })); 92 | 93 | export type AuthInstance = InstanceType; 94 | -------------------------------------------------------------------------------- /packages/aws/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @notation/aws 2 | 3 | ## 0.11.1 4 | 5 | ### Patch Changes 6 | 7 | - Hide large buffers from state file 8 | - Updated dependencies 9 | - @notation/aws.iac@0.11.1 10 | - @notation/core@0.11.1 11 | - @notation/std.iac@0.11.1 12 | 13 | ## 0.11.0 14 | 15 | ### Minor Changes 16 | 17 | - 5dcb935: Support alternative runtimes 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [5dcb935] 22 | - @notation/aws.iac@0.11.0 23 | - @notation/core@0.11.0 24 | - @notation/std.iac@0.11.0 25 | 26 | ## 0.10.0 27 | 28 | ### Minor Changes 29 | 30 | - Fix package versions 31 | 32 | ### Patch Changes 33 | 34 | - Updated dependencies 35 | - @notation/aws.iac@0.10.0 36 | - @notation/core@0.10.0 37 | - @notation/std.iac@0.10.0 38 | 39 | ## 0.7.1 40 | 41 | ### Patch Changes 42 | 43 | - Updated dependencies 44 | - @notation/core@0.6.1 45 | - @notation/aws.iac@0.6.1 46 | - @notation/std.iac@0.6.1 47 | 48 | ## 0.7.0 49 | 50 | ### Minor Changes 51 | 52 | - Support externally managed lambda modules 53 | 54 | ### Patch Changes 55 | 56 | - Updated dependencies 57 | - @notation/aws.iac@0.6.0 58 | - @notation/std.iac@0.6.0 59 | 60 | ## 0.6.0 61 | 62 | ### Minor Changes 63 | 64 | - 2a6fc59: Add optional JWT authorizer config to route resource 65 | 66 | ### Patch Changes 67 | 68 | - Updated dependencies [2a6fc59] 69 | - @notation/aws.iac@0.5.0 70 | - @notation/std.iac@0.5.0 71 | - @notation/core@0.6.0 72 | 73 | ## 0.5.3 74 | 75 | ### Patch Changes 76 | 77 | - Fix updating std.zip resource 78 | - Updated dependencies 79 | - @notation/aws.iac@0.4.3 80 | - @notation/core@0.5.1 81 | - @notation/std.iac@0.4.3 82 | 83 | ## 0.5.2 84 | 85 | ### Patch Changes 86 | 87 | - Fix types 88 | 89 | ## 0.5.0 90 | 91 | ### Minor Changes 92 | 93 | - 5debdd1: Show deployed resource state in dashboard 94 | 95 | ### Patch Changes 96 | 97 | - Updated dependencies [5debdd1] 98 | - @notation/core@0.5.0 99 | - @notation/aws.iac@0.4.2 100 | - @notation/std.iac@0.4.2 101 | 102 | ## 0.4.1 103 | 104 | ### Patch Changes 105 | 106 | - Removed dev artifacts from dist 107 | - Updated dependencies 108 | - @notation/aws.iac@0.4.1 109 | - @notation/core@0.4.1 110 | - @notation/std.iac@0.4.1 111 | 112 | ## 0.4.0 113 | 114 | ### Minor Changes 115 | 116 | - Stateful deployments 117 | 118 | ### Patch Changes 119 | 120 | - Updated dependencies 121 | - @notation/aws.iac@0.4.0 122 | - @notation/core@0.4.0 123 | - @notation/std.iac@0.4.0 124 | 125 | ## 0.3.1 126 | 127 | ### Patch Changes 128 | 129 | - Updated dependencies [b75f89b] 130 | - @notation/core@0.3.1 131 | - @notation/aws.iac@0.3.1 132 | - @notation/std.iac@0.3.1 133 | 134 | ## 0.3.0 135 | 136 | ### Minor Changes 137 | 138 | - Prepare for release 139 | 140 | ### Patch Changes 141 | 142 | - Updated dependencies 143 | - @notation/aws.iac@0.3.0 144 | - @notation/core@0.3.0 145 | - @notation/std.iac@0.3.0 146 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/test/plugins/function-infra-plugin.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { stripIndent } from "common-tags"; 3 | import { functionInfraPlugin } from "src/plugins/function-infra-plugin"; 4 | import { createBuilder } from "test/esbuild-test-utils"; 5 | 6 | const buildInfra = createBuilder((input) => ({ 7 | entryPoints: ["entry.fn.ts"], 8 | plugins: [functionInfraPlugin({ getFile: () => input })], 9 | })); 10 | 11 | it.skip("remaps exports", async () => { 12 | const input = ` 13 | import { handler } from "@notation/aws/api-gateway"; 14 | export const config = { service: "aws/lambda" }; 15 | export const getNum = handler(() => 1); 16 | `; 17 | 18 | const expected = stripIndent` 19 | import { lambda } from "@notation/aws/lambda"; 20 | const config = { service: "aws/lambda" }; 21 | export const getNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getNum", ...config }); 22 | `; 23 | 24 | const output = await buildInfra(input); 25 | 26 | expect(output).toContain(expected); 27 | }); 28 | 29 | it.skip("merges config", async () => { 30 | const input = ` 31 | import { LambdaConfig } from "@notation/aws/lambda"; 32 | import { handler } from "@notation/aws/api-gateway"; 33 | export const getNum = handler(() => 1); 34 | export const config: LambdaConfig = { service: "aws/lambda", memory: 64 }; 35 | `; 36 | 37 | const expected = stripIndent` 38 | import { lambda } from "@notation/aws/lambda"; 39 | const config = { service: "aws/lambda", memory: 64 }; 40 | export const getNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getNum", ...config }); 41 | `; 42 | 43 | const output = await buildInfra(input); 44 | 45 | expect(output).toContain(expected); 46 | }); 47 | 48 | it.skip("should strip runtime code", async () => { 49 | const input = ` 50 | import { LambdaConfig } from "@notation/aws/lambda"; 51 | import { handler } from "@notation/aws/api-gateway"; 52 | import lib from "lib"; 53 | 54 | let num = lib.getNum(); 55 | 56 | export const getNum = () => num; 57 | export const getDoubleNum = () => num * 2; 58 | export const config = { service: "aws/lambda" };`; 59 | 60 | const expected = stripIndent` 61 | import { lambda } from "@notation/aws/lambda"; 62 | const config = { service: "aws/lambda" }; 63 | export const getNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getNum", ...config }); 64 | export const getDoubleNum = lambda({ fileName: "dist/runtime/entry.fn/index.mjs", handler: "getDoubleNum", ...config }); 65 | `; 66 | 67 | const output = await buildInfra(input); 68 | 69 | expect(output).toContain(expected); 70 | }); 71 | 72 | // broken in bun 1.0.7 73 | it.skip("should fail if no config is exported", async () => { 74 | const input = ` 75 | import { handler } from "@notation/aws/api-gateway"; 76 | export const getNum = handler(() => 1); 77 | `; 78 | 79 | expect(buildInfra(input)).rejects.toThrow(/No config object was exported/); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/lambda/lambda-role.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import * as sdk from "@aws-sdk/client-iam"; 3 | import * as z from "zod"; 4 | import { iamClient } from "src/utils/aws-clients"; 5 | import { AwsSchema } from "src/utils/types"; 6 | import { lambdaTrustPolicy } from "src/templates/iam.policy"; 7 | 8 | export type LambdaIamRoleSchema = AwsSchema<{ 9 | Key: sdk.DeleteRoleRequest; 10 | CreateParams: sdk.CreateRoleRequest; 11 | UpdateParams: sdk.UpdateRoleRequest; 12 | ReadResult: NonNullable; 13 | }>; 14 | 15 | const lambdaIamRole = resource({ 16 | type: "aws/lambda/LambdaIamRole", 17 | }); 18 | 19 | const lambdaIamRoleSchema = lambdaIamRole.defineSchema({ 20 | RoleName: { 21 | valueType: z.string(), 22 | propertyType: "param", 23 | presence: "required", 24 | primaryKey: true, 25 | }, 26 | Arn: { 27 | valueType: z.string(), 28 | propertyType: "computed", 29 | presence: "required", 30 | }, 31 | AssumeRolePolicyDocument: { 32 | valueType: z.string(), 33 | propertyType: "param", 34 | presence: "required", 35 | }, 36 | CreateDate: { 37 | valueType: z.date(), 38 | propertyType: "computed", 39 | presence: "required", 40 | volatile: true, 41 | }, 42 | Description: { 43 | valueType: z.string(), 44 | propertyType: "param", 45 | presence: "optional", 46 | }, 47 | MaxSessionDuration: { 48 | valueType: z.number(), 49 | propertyType: "param", 50 | presence: "optional", 51 | }, 52 | Path: { 53 | valueType: z.string(), 54 | propertyType: "param", 55 | presence: "optional", 56 | }, 57 | PermissionsBoundary: { 58 | valueType: z.string(), 59 | propertyType: "param", 60 | presence: "optional", 61 | }, 62 | RoleId: { 63 | valueType: z.string(), 64 | propertyType: "computed", 65 | presence: "required", 66 | }, 67 | RoleLastUsed: { 68 | valueType: z.object({ 69 | LastUsedDate: z.date().optional(), 70 | Region: z.string().optional(), 71 | }), 72 | propertyType: "computed", 73 | presence: "optional", 74 | }, 75 | } as const); 76 | 77 | export const LambdaIamRole = lambdaIamRoleSchema.defineOperations({ 78 | create: async (params) => { 79 | const command = new sdk.CreateRoleCommand(params); 80 | await iamClient.send(command); 81 | }, 82 | read: async (key) => { 83 | const command = new sdk.GetRoleCommand(key); 84 | const { Role } = await iamClient.send(command); 85 | return Role!; 86 | }, 87 | update: async (key, params) => { 88 | const command = new sdk.UpdateRoleCommand({ ...key, ...params }); 89 | await iamClient.send(command); 90 | }, 91 | delete: async (key) => { 92 | const command = new sdk.DeleteRoleCommand(key); 93 | await iamClient.send(command); 94 | }, 95 | setIntrinsicConfig: () => ({ 96 | AssumeRolePolicyDocument: JSON.stringify(lambdaTrustPolicy), 97 | }), 98 | }); 99 | 100 | export type LambdaIamRoleInstance = InstanceType; 101 | -------------------------------------------------------------------------------- /packages/aws/src/api-gateway/route.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApiGatewayHandler, 3 | JWTAuthorizedApiGatewayHandler, 4 | } from "src/shared"; 5 | import * as aws from "@notation/aws.iac"; 6 | import { api } from "./api"; 7 | import { AuthorizerConfig } from "./auth"; 8 | import { mapAuthConfig, mapAuthType } from "./utils"; 9 | 10 | export const route = ( 11 | apiGroup: ReturnType, 12 | method: string, // todo: http methods only 13 | path: `/${string}`, 14 | auth: AuthorizerConfig, 15 | handler: 16 | | ApiGatewayHandler 17 | | JWTAuthorizedApiGatewayHandler 18 | // todo: narrow to lambda group 19 | | aws.AwsResourceGroup, 20 | ) => { 21 | const apiResource = apiGroup.findResource(aws.apiGateway.Api)!; 22 | const routeId = `${apiResource.id}-${method}-${path}`; 23 | 24 | const lambdaGroup = 25 | handler instanceof aws.AwsResourceGroup 26 | ? handler 27 | : // at compile time, runtime module becomes infra resource group 28 | (handler as any as aws.AwsResourceGroup); 29 | 30 | const lambdaResource = lambdaGroup.findResource(aws.lambda.LambdaFunction)!; 31 | 32 | let integration = lambdaGroup.findResource(aws.apiGateway.LambdaIntegration); 33 | 34 | if (!integration) { 35 | integration = lambdaGroup.add( 36 | new aws.apiGateway.LambdaIntegration({ 37 | id: `${apiResource.id}-${lambdaResource.id}-integration`, 38 | dependencies: { 39 | api: apiResource, 40 | lambda: lambdaResource, 41 | }, 42 | }), 43 | ); 44 | } 45 | 46 | const permission = lambdaGroup.findResource( 47 | aws.lambda.LambdaApiGatewayV2Permission, 48 | ); 49 | 50 | if (!permission) { 51 | lambdaGroup.add( 52 | new aws.lambda.LambdaApiGatewayV2Permission({ 53 | id: `${lambdaResource.id}-${apiResource.id}-permission`, 54 | dependencies: { 55 | api: apiResource, 56 | lambda: lambdaResource, 57 | }, 58 | }), 59 | ); 60 | } 61 | 62 | const routeGroup = new aws.AwsResourceGroup("API Gateway/Route", { 63 | dependencies: { router: apiGroup.id, fn: lambdaGroup.id }, 64 | }); 65 | 66 | if (auth.type != "NONE") { 67 | const authConfig = mapAuthConfig(apiResource.id, method, path, auth); 68 | 69 | const authorizer = new aws.apiGateway.RouteAuth({ 70 | id: `${routeId}-${apiResource.id}-authorizer`, 71 | config: authConfig, 72 | dependencies: { 73 | api: apiResource, 74 | }, 75 | }); 76 | 77 | routeGroup.add(authorizer); 78 | } 79 | 80 | const authorizerResource = routeGroup.findResource(aws.apiGateway.RouteAuth); 81 | 82 | routeGroup.add( 83 | new aws.apiGateway.Route({ 84 | id: routeId, 85 | config: { 86 | AuthorizationScopes: auth.scopes, 87 | RouteKey: `${method} ${path}`, 88 | AuthorizationType: mapAuthType(auth), 89 | }, 90 | dependencies: { 91 | api: apiResource, 92 | lambdaIntegration: integration, 93 | auth: authorizerResource, 94 | }, 95 | }), 96 | ); 97 | 98 | return routeGroup; 99 | }; 100 | -------------------------------------------------------------------------------- /packages/cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @notation/cli 2 | 3 | ## 0.11.1 4 | 5 | ### Patch Changes 6 | 7 | - Hide large buffers from state file 8 | - Updated dependencies 9 | - @notation/core@0.11.1 10 | - @notation/dashboard@0.11.1 11 | - @notation/esbuild-plugins@0.11.1 12 | 13 | ## 0.11.0 14 | 15 | ### Patch Changes 16 | 17 | - @notation/core@0.11.0 18 | - @notation/dashboard@0.11.0 19 | - @notation/esbuild-plugins@0.11.0 20 | 21 | ## 0.10.0 22 | 23 | ### Minor Changes 24 | 25 | - Fix package versions 26 | 27 | ### Patch Changes 28 | 29 | - Updated dependencies 30 | - @notation/core@0.10.0 31 | - @notation/dashboard@0.10.0 32 | - @notation/esbuild-plugins@0.10.0 33 | 34 | ## 0.9.1 35 | 36 | ### Patch Changes 37 | 38 | - Updated dependencies 39 | - @notation/core@0.6.1 40 | - @notation/dashboard@0.3.0 41 | - @notation/esbuild-plugins@0.6.1 42 | 43 | ## 0.9.0 44 | 45 | ### Minor Changes 46 | 47 | - Fix dashboard 48 | 49 | ### Patch Changes 50 | 51 | - Updated dependencies 52 | - @notation/dashboard@0.3.0 53 | 54 | ## 0.8.0 55 | 56 | ### Minor Changes 57 | 58 | - Support externally managed lambda modules 59 | 60 | ### Patch Changes 61 | 62 | - Updated dependencies 63 | - @notation/esbuild-plugins@0.6.0 64 | 65 | ## 0.7.0 66 | 67 | ### Minor Changes 68 | 69 | - 2a6fc59: Add optional JWT authorizer config to route resource 70 | 71 | ### Patch Changes 72 | 73 | - Handle missing credentials 74 | - Updated dependencies [2a6fc59] 75 | - @notation/esbuild-plugins@0.5.0 76 | - @notation/core@0.6.0 77 | - @notation/dashboard@0.2.0 78 | 79 | ## 0.6.2 80 | 81 | ### Patch Changes 82 | 83 | - Fix updating std.zip resource 84 | - Updated dependencies 85 | - @notation/core@0.5.1 86 | - @notation/dashboard@0.2.0 87 | - @notation/esbuild-plugins@0.4.2 88 | 89 | ## 0.6.1 90 | 91 | ### Patch Changes 92 | 93 | - Updated dependencies [5debdd1] 94 | - @notation/dashboard@0.2.0 95 | - @notation/core@0.5.0 96 | - @notation/esbuild-plugins@0.4.1 97 | 98 | ## 0.6.0 99 | 100 | ### Minor Changes 101 | 102 | - Load infra in runtime modules 103 | 104 | ### Patch Changes 105 | 106 | - 10a8133: Migrate project scaffolding to create-notation 107 | - Updated dependencies 108 | - Updated dependencies 109 | - Updated dependencies 110 | - @notation/dashboard@0.1.0 111 | - @notation/esbuild-plugins@0.4.0 112 | - @notation/core@0.4.1 113 | 114 | ## 0.5.0 115 | 116 | ### Minor Changes 117 | 118 | - Stateful deployments 119 | 120 | ### Patch Changes 121 | 122 | - Updated dependencies 123 | - @notation/core@0.4.0 124 | - @notation/esbuild-plugins@0.3.2 125 | 126 | ## 0.4.2 127 | 128 | ### Patch Changes 129 | 130 | - Fix tsconfig and getting started instruction 131 | 132 | ## 0.4.1 133 | 134 | ### Patch Changes 135 | 136 | - 3b82ef1: Include templates in package bundle 137 | 138 | ## 0.4.0 139 | 140 | ### Minor Changes 141 | 142 | - b75f89b: Add create app command 143 | 144 | ### Patch Changes 145 | 146 | - Updated dependencies [b75f89b] 147 | - @notation/core@0.3.1 148 | - @notation/esbuild-plugins@0.3.1 149 | 150 | ## 0.3.0 151 | 152 | ### Minor Changes 153 | 154 | - Prepare for release 155 | 156 | ### Patch Changes 157 | 158 | - Updated dependencies 159 | - @notation/core@0.3.0 160 | - @notation/esbuild-plugins@0.3.0 161 | -------------------------------------------------------------------------------- /packages/core/test/visualiser/chart.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | import { stripIndent } from "common-tags"; 3 | import { 4 | createMermaidFlowChart, 5 | createMermaidLiveUrl, 6 | } from "src/visualiser/chart"; 7 | import { ResourceGroup } from "src/orchestrator/resource-group"; 8 | import { Resource } from "src/orchestrator/resource"; 9 | 10 | it("should create a mermaid flowchart string", () => { 11 | const { resourceGroups, resources } = getFixture(); 12 | const chart = createMermaidFlowChart(resourceGroups, resources); 13 | 14 | const expected = stripIndent` 15 | flowchart TD 16 | subgraph GroupTypeA_0 17 | ResourceTypeA_0(ResourceTypeA) 18 | ResourceTypeB_1(ResourceTypeB) 19 | end 20 | subgraph GroupTypeB_1 21 | ResourceTypeC_2(ResourceTypeC) 22 | end 23 | 24 | ResourceTypeB_1 --> ResourceTypeA_0`; 25 | 26 | expect(chart).toBe(expected + "\n"); 27 | }); 28 | 29 | it("should create a mermaid live URL from given code", () => { 30 | const mermaidCode = "flowchart TD\nA --> B"; 31 | const result = createMermaidLiveUrl(mermaidCode); 32 | const expected = 33 | "https://mermaid.live/view#pako:eNolyksKhDAQRdGtFG-sG8igQXEH9rAmRVJ-wCRNqNCIuHcDzi6Xc8HnoHBYjvz3mxSj78RpoL7_0IgOUUuUPTRxcSJi2KZRGa5l0EXqYQxOd6NSLc9n8nBWqnaovyCm0y5rkfjO-wHolSVC"; 34 | expect(result).toBe(expected); 35 | }); 36 | 37 | function getFixture() { 38 | const resourceGroups = [ 39 | { 40 | id: 0, 41 | type: "GroupTypeA", 42 | config: {}, 43 | dependencies: {}, 44 | resources: [ 45 | { 46 | id: 0, 47 | groupId: 0, 48 | config: {}, 49 | type: "ResourceTypeA", 50 | dependencies: {}, 51 | }, 52 | { 53 | id: 1, 54 | groupId: 0, 55 | config: {}, 56 | type: "ResourceTypeB", 57 | dependencies: { 58 | dep1: { 59 | id: 0, 60 | groupId: 0, 61 | input: {}, 62 | type: "ResourceTypeA", 63 | dependencies: {}, 64 | }, 65 | }, 66 | }, 67 | ], 68 | }, 69 | { 70 | id: 1, 71 | type: "GroupTypeB", 72 | config: {}, 73 | dependencies: {}, 74 | resources: [ 75 | { 76 | id: 2, 77 | groupId: 1, 78 | input: {}, 79 | type: "ResourceTypeC", 80 | dependencies: {}, 81 | }, 82 | ], 83 | }, 84 | ] as unknown as ResourceGroup[]; 85 | 86 | const resources = [ 87 | { 88 | id: 0, 89 | groupId: 0, 90 | config: {}, 91 | type: "ResourceTypeA", 92 | dependencies: {}, 93 | }, 94 | { 95 | id: 1, 96 | groupId: 0, 97 | config: {}, 98 | type: "ResourceTypeB", 99 | dependencies: { 100 | dep1: { 101 | id: 0, 102 | groupId: 0, 103 | input: {}, 104 | type: "ResourceTypeA", 105 | dependencies: {}, 106 | }, 107 | }, 108 | }, 109 | { 110 | id: 2, 111 | groupId: 1, 112 | config: {}, 113 | type: "ResourceTypeC", 114 | dependencies: {}, 115 | }, 116 | ] as unknown as Resource[]; 117 | 118 | return { resourceGroups, resources }; 119 | } 120 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/src/parsers/remove-unsafe-references.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function removeUnsafeReferences(sourceText: string): string { 4 | const sourceFile = ts.createSourceFile( 5 | "file.ts", 6 | sourceText, 7 | ts.ScriptTarget.ES2015, 8 | true, 9 | ); 10 | const printer = ts.createPrinter(); 11 | 12 | let infraImports: Set = new Set(); 13 | 14 | function transformer(context: ts.TransformationContext) { 15 | return (node: ts.Node, isTopLevel: boolean = true): ts.Node => { 16 | if ( 17 | ts.isImportDeclaration(node) && 18 | ts.isStringLiteral(node.moduleSpecifier) 19 | ) { 20 | const path = node.moduleSpecifier.text; 21 | 22 | // todo: enable relative paths by resolving path to validate location 23 | if (path.startsWith("infra/")) { 24 | node.importClause?.namedBindings?.forEachChild((namedBinding) => { 25 | if (ts.isImportSpecifier(namedBinding)) { 26 | infraImports.add(namedBinding.name.text); 27 | } 28 | }); 29 | return node; 30 | } else { 31 | return ts.factory.createNotEmittedStatement(node); 32 | } 33 | } 34 | 35 | if ( 36 | ts.isStatement(node) && 37 | containsUnsafeReferences(node, infraImports) 38 | ) { 39 | return ts.factory.createNotEmittedStatement(node); 40 | } 41 | 42 | return ts.visitEachChild( 43 | node, 44 | (child) => transformer(context)(child), 45 | context, 46 | ); 47 | }; 48 | } 49 | 50 | const transformedSourceFile = ts.transform(sourceFile, [transformer]) 51 | .transformed[0]; 52 | 53 | return printer.printFile(transformedSourceFile as ts.SourceFile); 54 | } 55 | 56 | function containsUnsafeReferences( 57 | node: ts.Node, 58 | safeReferences: Set, 59 | ): boolean { 60 | let hasUnsafeReference = false; 61 | 62 | function visit(n: ts.Node, parent: ts.Node | undefined = undefined): void { 63 | if (ts.isIdentifier(n)) { 64 | if (isReference(n, parent) && !safeReferences.has(n.text)) { 65 | hasUnsafeReference = true; 66 | } 67 | } else { 68 | n.forEachChild((child) => visit(child, n)); 69 | } 70 | } 71 | 72 | visit(node); 73 | 74 | return hasUnsafeReference; 75 | } 76 | 77 | function isReference( 78 | node: ts.Identifier, 79 | parent: ts.Node | undefined, 80 | ): boolean { 81 | if (!parent) { 82 | return false; 83 | } 84 | 85 | if (ts.isTypeReferenceNode(parent)) { 86 | return false; 87 | } 88 | 89 | if (ts.isTypeParameterDeclaration(parent) && parent.name === node) { 90 | return false; 91 | } 92 | 93 | if ( 94 | (ts.isInterfaceDeclaration(parent) || ts.isTypeAliasDeclaration(parent)) && 95 | parent.name === node 96 | ) { 97 | return false; 98 | } 99 | 100 | if (ts.isPropertyAssignment(parent) && parent.name === node) { 101 | return false; 102 | } 103 | 104 | if (ts.isPropertyAccessExpression(parent) && parent.name === node) { 105 | return false; 106 | } 107 | 108 | if (ts.isMethodDeclaration(parent) && parent.name === node) { 109 | return false; 110 | } 111 | 112 | if (ts.isParameter(parent) && parent.name === node) { 113 | return false; 114 | } 115 | 116 | if (ts.isVariableDeclaration(parent) && parent.name === node) { 117 | return false; 118 | } 119 | 120 | if (ts.isFunctionDeclaration(parent) && parent.name === node) { 121 | return false; 122 | } 123 | 124 | return true; 125 | } 126 | -------------------------------------------------------------------------------- /packages/core/test/orchestrator/resource-group.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, expect, it } from "vitest"; 2 | import { ResourceGroup, getResourceGroups, getResources } from "src"; 3 | import { reset } from "src/"; 4 | import { 5 | TestResource, 6 | TestResource2, 7 | testResourceConfig, 8 | } from "./resource.doubles"; 9 | 10 | beforeEach(() => { 11 | reset(); 12 | }); 13 | 14 | class TestResourceGroup extends ResourceGroup { 15 | platform = "test-platform"; 16 | } 17 | 18 | const testResource = new TestResource({ 19 | id: "test-resource-1", 20 | config: testResourceConfig, 21 | }); 22 | 23 | const testResource2 = new TestResource2({ 24 | id: "test-resource-2", 25 | config: testResourceConfig, 26 | }); 27 | 28 | it("creates a resource group", () => { 29 | const resourceGroup = new TestResourceGroup("test-group", { a: 1 }); 30 | expect(resourceGroup.id).toBe(0); 31 | expect(resourceGroup.type).toBe("test-group"); 32 | expect(resourceGroup.platform).toBe("test-platform"); 33 | expect(resourceGroup.config).toEqual({ a: 1 }); 34 | expect(resourceGroup.resources).toEqual([]); 35 | }); 36 | 37 | it("stores resource groups in the global array", () => { 38 | new TestResourceGroup("test-group", { type: "test1" }); 39 | new TestResourceGroup("test-group-2", { type: "test2" }); 40 | 41 | expect(getResourceGroups()).toHaveLength(2); 42 | expect(getResourceGroups()[0].type).toBe("test-group"); 43 | expect(getResourceGroups()[1].type).toBe("test-group-2"); 44 | }); 45 | 46 | it("creates a resource within a group", () => { 47 | const resourceGroup = new TestResourceGroup("test-group", {}); 48 | const resource = resourceGroup.add(testResource); 49 | expect(resourceGroup.resources).toContain(resource); 50 | }); 51 | 52 | it("stores resources in the global array", () => { 53 | const resourceGroup = new TestResourceGroup("test-group", {}); 54 | resourceGroup.add(testResource); 55 | resourceGroup.add(testResource2); 56 | 57 | expect(getResources()).toHaveLength(2); 58 | expect(getResources()[0]).toBe(testResource); 59 | expect(getResources()[1]).toBe(testResource2); 60 | }); 61 | 62 | it("increments resource group IDs", () => { 63 | const rg1 = new TestResourceGroup("test-group", { type: "group1" }); 64 | const rg2 = new TestResourceGroup("test-group", { type: "group2" }); 65 | 66 | expect(rg1.id).toBe(0); 67 | expect(rg2.id).toBe(1); 68 | }); 69 | 70 | it("finds a resource within a group", () => { 71 | const resourceGroup = new TestResourceGroup("test-group", { type: "group1" }); 72 | const resource = resourceGroup.add(testResource); 73 | 74 | expect(resourceGroup.findResource(TestResource)).toBe(resource); 75 | expect(resourceGroup.findResource(TestResource2)).toBe(undefined); 76 | }); 77 | 78 | it("references resources within groups", () => { 79 | const rg1 = new TestResourceGroup("test-group", {}); 80 | const r1 = rg1.add(testResource); 81 | const r2 = rg1.add(testResource2); 82 | 83 | expect(getResources()).toContain(r1); 84 | expect(getResources()).toContain(r2); 85 | }); 86 | 87 | it("throws an error when adding an existing resource", () => { 88 | const rg1 = new TestResourceGroup("test-group", {}); 89 | rg1.add(testResource); 90 | expect(() => rg1.add(testResource)).toThrow(); 91 | }); 92 | 93 | it("increments resource IDs globally", () => { 94 | const rg1 = new TestResourceGroup("test-group", {}); 95 | const r1 = rg1.add(testResource); 96 | const rg2 = new TestResourceGroup("test-group", {}); 97 | const r2 = rg2.add(testResource2); 98 | expect(r1.id).toBe("test-resource-1"); 99 | expect(r2.id).toBe("test-resource-2"); 100 | }); 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | Notation Logo 7 | 8 | 9 |
10 | 11 | 12 | Discussions 13 | Discord 14 |

15 | 16 | ### JavaScript-native developer toolkit for cloud-native deployments 17 | 18 | Notation is an experimental serverless toolkit designed for JavaScript's culture and ecosystem. 19 | 20 | [Website](https://notation.dev) •  21 | [Quick Start](#quick-start) •  22 | [Slow Start](https://notation.dev/docs/guide) •  23 | [Examples](https://github.com/notation-dev/notation/tree/main/examples) •  24 | [Discord](https://discord.gg/mGzDWShPzm) •  25 | [Twitter](https://twitter.com/notation_dev) 26 | 27 |
28 | 29 | ## Features 30 | 31 | ### Infrastructure compiler 32 | 33 | Generates cloud implementation details from intuitive-to-write code. 34 | 35 | Using inference and best practices, compiles policy documents, ARN strings, IAM roles, permissions etc. 36 | 37 | Notation infra graph 38 | 39 |
40 | 41 | ### Hot infra replacement 42 | 43 | Deploys both production and ephemeral dev stacks. 44 | 45 | Built from the ground up and designed for fast iteration cycles, hot infra replacement updates live dev stacks while you code. 46 | 47 | https://github.com/notation-dev/notation/assets/1670902/fd8c955f-8aa9-4800-813a-ea96c2b398cc 48 | 49 | ### End-to-end types 50 | 51 | Brings together popular serverless technologies in a unified type space. 52 | 53 | Provides well-designed types for every resource, and ensures compatibility between infrastructure and runtime modules. 54 | 55 | Notation TS error 56 | 57 | ## Quick Start 58 | 59 | ```sh 60 | npm create notation@alpha my-app 61 | ``` 62 | 63 | See also: [Getting Started Guide](https://notation.dev/docs/guide) 64 | 65 | ## Demo 66 | 67 | A walkthrough of the key features of Notation. 68 | 69 | 70 | Notation demo thumbnail 71 | 72 | 73 | ## Community 74 | 75 | - **[Discussions](https://github.com/notation-dev/notation/discussions)**: ask questions, give feedback on RFCs, suggest ideas 76 | - **[Issues](https://github.com/notation-dev/notation/issues/new)**: report bugs, 77 | suggest new features, or help us improve the docs 78 | - **[Discord](https://discord.gg/mGzDWShPzm)**: ask for advice, share your projects, 79 | discuss contributions 80 | 81 | ## Questions and Feedback 82 | 83 | Feel free to book a call to discuss Notation. We'd love to hear your feedback or answer any questions you have. 84 | 85 | Book us with Cal.com 86 | 87 | ## License 88 | 89 | [Apache 2.0](https://choosealicense.com/licenses/apache-2.0/) 90 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/src/parsers/parse-fn-module.ts: -------------------------------------------------------------------------------- 1 | import ts from "typescript"; 2 | 3 | export function parseFnModule(input: string) { 4 | const sourceFile = ts.createSourceFile( 5 | "file.ts", 6 | input, 7 | ts.ScriptTarget.ESNext, 8 | true, 9 | ); 10 | 11 | let exports: ReturnType = []; 12 | 13 | for (const statement of sourceFile.statements) { 14 | exports = exports.concat(getExportsFromStatement(statement)); 15 | } 16 | 17 | const configExport = exports.find((exp) => exp.name === "config"); 18 | 19 | if (!configExport) { 20 | throw new Error("A config object was not exported"); 21 | } 22 | 23 | const { config, configRaw } = createConfigObject(configExport.node); 24 | 25 | return { 26 | config, 27 | configRaw, 28 | exports: exports.map((exp) => exp.name), 29 | }; 30 | } 31 | 32 | function getExportsFromStatement( 33 | node: ts.Node, 34 | ): Array<{ name: string; node: ts.Node }> { 35 | const exports: Array<{ name: string; node: ts.Node }> = []; 36 | 37 | if (ts.isVariableStatement(node)) { 38 | const hasExportModifier = 39 | node.modifiers && 40 | node.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ExportKeyword); 41 | if (hasExportModifier) { 42 | for (const declaration of node.declarationList.declarations) { 43 | if (!declaration.initializer) { 44 | throw new Error("Exports must be assigned a value"); 45 | } 46 | exports.push({ 47 | name: declaration.name.getText(), 48 | node: declaration.initializer, 49 | }); 50 | } 51 | } 52 | } else if ( 53 | ts.isFunctionDeclaration(node) && 54 | node.name && 55 | ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export 56 | ) { 57 | exports.push({ 58 | name: node.name.getText(), 59 | node: node, 60 | }); 61 | } else if (ts.isExportAssignment(node) && node.expression) { 62 | throw new Error("Default exports are not supported"); 63 | } else if ( 64 | ts.isExportDeclaration(node) && 65 | node.exportClause && 66 | ts.isNamedExports(node.exportClause) 67 | ) { 68 | throw new Error("Re-exporting is not supported"); 69 | } else if ( 70 | ts.isClassDeclaration(node) && 71 | node.name && 72 | ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export 73 | ) { 74 | throw new Error("Class exports are not supported"); 75 | } 76 | return exports; 77 | } 78 | 79 | function createConfigObject(node: ts.Node) { 80 | if (!ts.isObjectLiteralExpression(node)) { 81 | throw new Error("'config' is not an object literal."); 82 | } 83 | let configRaw = "{ "; 84 | let config: Record = {}; 85 | node.properties.forEach((prop, index, array) => { 86 | if (ts.isPropertyAssignment(prop)) { 87 | const key = prop.name.getText(); 88 | const valueNode = prop.initializer; 89 | if ( 90 | ts.isStringLiteral(valueNode) || 91 | ts.isNumericLiteral(valueNode) || 92 | valueNode.kind === ts.SyntaxKind.TrueKeyword || 93 | valueNode.kind === ts.SyntaxKind.FalseKeyword 94 | ) { 95 | const value = valueNode.getText(); 96 | configRaw += `${key}: ${value}`; 97 | configRaw += index === array.length - 1 ? " }" : ", "; 98 | config[key] = JSON.parse(value); 99 | } else { 100 | throw new Error( 101 | `Invalid value type for key '${key}': only numbers, strings, and booleans are allowed.`, 102 | ); 103 | } 104 | } else { 105 | throw new Error( 106 | `Invalid property assignment in 'config': ${prop.getText()}`, 107 | ); 108 | } 109 | }); 110 | 111 | return { config, configRaw }; 112 | } 113 | -------------------------------------------------------------------------------- /packages/esbuild-plugins/test/parsers/parse-fn-module.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, test } from "vitest"; 2 | import { parseFnModule } from "src/parsers/parse-fn-module"; 3 | 4 | describe("parsing exports", () => { 5 | it("gets export identifiers", () => { 6 | const input = ` 7 | export const getNum = handler(() => num); 8 | export const getDoubleNum = handler(() => num * 2); 9 | export const config = {}; 10 | const num = 123; 11 | `; 12 | 13 | const { exports } = parseFnModule(input); 14 | 15 | expect(exports).toEqual(["getNum", "getDoubleNum", "config"]); 16 | }); 17 | 18 | it("does not allow default exports", () => { 19 | const input = `export default {};`; 20 | 21 | expect(() => parseFnModule(input)).toThrow( 22 | /Default exports are not supported/, 23 | ); 24 | }); 25 | 26 | it("does not allow export declarations", () => { 27 | const inputs = [ 28 | `export { something } from "./something";`, 29 | `export { a };`, 30 | ]; 31 | 32 | inputs.forEach((input) => { 33 | expect(() => parseFnModule(input)).toThrow( 34 | /Re-exporting is not supported/, 35 | ); 36 | }); 37 | }); 38 | 39 | it("does not allow class exports", () => { 40 | const input = `export class MyClass {};`; 41 | expect(() => parseFnModule(input)).toThrow( 42 | /Class exports are not supported/, 43 | ); 44 | }); 45 | }); 46 | 47 | describe("parsing config", () => { 48 | it("parses primitive properties", () => { 49 | const input = ` 50 | export const config = { 51 | key1: "value1", 52 | key2: 123, 53 | key3: true, 54 | key4: false 55 | }; 56 | `; 57 | 58 | const { config } = parseFnModule(input); 59 | 60 | expect(config).toEqual({ 61 | key1: "value1", 62 | key2: 123, 63 | key3: true, 64 | key4: false, 65 | }); 66 | }); 67 | 68 | it("does not allow complex property types", () => { 69 | const inputs = [ 70 | `export const config = { key1: [123] };`, 71 | `export const config = { key1: {} };`, 72 | `export const config = { key1: ref };`, 73 | ]; 74 | for (const input of inputs) { 75 | expect(() => parseFnModule(input)).toThrow( 76 | /Invalid value type for key 'key1'/, 77 | ); 78 | } 79 | }); 80 | 81 | it("throws an error if no config is provided", async () => { 82 | const input = ` 83 | import { handler } from "@notation/aws/api-gateway"; 84 | export const getNum = handler(() => 1); 85 | `; 86 | expect(() => parseFnModule(input)).toThrow( 87 | /A config object was not exported/, 88 | ); 89 | }); 90 | 91 | it("returns a raw config string", () => { 92 | const input = `export const config = { 93 | key1: "value1", 94 | key2: 123, 95 | key3: true, 96 | key4: false 97 | };`; 98 | 99 | const { configRaw } = parseFnModule(input); 100 | 101 | expect(configRaw).toBe( 102 | `{ key1: "value1", key2: 123, key3: true, key4: false }`, 103 | ); 104 | }); 105 | 106 | describe("invalid config values", () => { 107 | const invalidInputs = { 108 | identifier: "export const config = identifier;", 109 | callExpression: "export const config = identifier;", 110 | string: "export const config = 123;", 111 | number: "export const config = '123';", 112 | array: "export const config = [];", 113 | }; 114 | for (const [invalidType, invalidStatement] of Object.entries( 115 | invalidInputs, 116 | )) { 117 | test(invalidType, () => { 118 | expect(() => parseFnModule(invalidStatement)).toThrow( 119 | /'config' is not an object literal./, 120 | ); 121 | }); 122 | } 123 | }); 124 | }); 125 | -------------------------------------------------------------------------------- /packages/core/src/provisioner/workflows/workflow.deploy.ts: -------------------------------------------------------------------------------- 1 | import { getResourceGraph } from "src/orchestrator/graph"; 2 | import { createResource } from "../operations/operation.create"; 3 | import { readResource } from "../operations/operation.read"; 4 | import { updateResource } from "../operations/operation.update"; 5 | import { deleteResource } from "../operations/operation.delete"; 6 | import { State } from "../state"; 7 | import * as deepDiff from "deep-object-diff"; 8 | import { BaseResource } from "src/orchestrator/resource"; 9 | 10 | export async function deployApp( 11 | entryPoint: string, 12 | driftDetection = true, 13 | dryRun = false, 14 | ): Promise { 15 | const graph = await getResourceGraph(entryPoint); 16 | const state = new State(); 17 | 18 | for (const resource of graph.resources) { 19 | const stateNode = await state.get(resource.id); 20 | 21 | // 1. Has resource been created before? 22 | if (!stateNode) { 23 | await createResource({ resource, state, dryRun }); 24 | continue; 25 | } 26 | 27 | // 2. Assign existing state output to resource 28 | // todo: get hidden fields (or read live resource and check drift first?) 29 | resource.setOutput(stateNode.output); 30 | 31 | // 3. Have the desired params changed from the state? 32 | const params = await resource.getParams(); 33 | 34 | // diff to transition from last state to current params 35 | const localDiff = deepDiff.diff( 36 | resource.toComparable(stateNode.params), 37 | resource.toComparable(params), 38 | ); 39 | 40 | // todo: what should happen if a property is undefined? 41 | // should it be ignored? 42 | const stateIsStale = Object.keys(localDiff).length > 0; 43 | 44 | if (stateIsStale) { 45 | console.log(`Resource ${resource.id} has changed. Updating...`); 46 | await updateResource({ resource, state, patch: localDiff, dryRun }); 47 | continue; 48 | } 49 | 50 | if (!driftDetection) continue; 51 | 52 | const latestOutput = await readResource({ 53 | resource, 54 | state, 55 | dryRun, 56 | quiet: true, 57 | }); 58 | 59 | // 4. Has resource been deleted? 60 | if (latestOutput === null) { 61 | console.log(`Resource ${resource.id} has been deleted remotely.`); 62 | await createResource({ resource, state, dryRun }); 63 | continue; 64 | } 65 | 66 | // 5. Have the params of the live resource drifted from the state? 67 | const remoteDetailedDiff = deepDiff.detailedDiff( 68 | resource.toComparable(latestOutput), 69 | resource.toComparable(stateNode.output), 70 | ); 71 | 72 | // diff to transition from live to declared state 73 | const remoteDiff = { 74 | ...remoteDetailedDiff.updated, 75 | ...remoteDetailedDiff.added, 76 | }; 77 | const resourceHasDrifted = Object.keys(remoteDiff).length > 0; 78 | 79 | if (resourceHasDrifted) { 80 | console.log( 81 | `Remote ${resource.id} has drifted from its state. Reverting...`, 82 | ); 83 | await updateResource({ resource, state, patch: remoteDiff, dryRun }); 84 | continue; 85 | } 86 | } 87 | 88 | // 6. Has resource been removed from the orchestration graph? 89 | const stateNodes = (await state.values()).reverse(); 90 | 91 | for (const stateNode of stateNodes.reverse()) { 92 | let resource = graph.resources.find((r) => r.id === stateNode.id); 93 | 94 | if (!resource) { 95 | const { moduleName, serviceName, resourceName } = stateNode.meta; 96 | const provider = await import(moduleName); 97 | const Resource = provider[serviceName][resourceName]; 98 | resource = new Resource({ config: stateNode.config }) as BaseResource; 99 | resource.id = stateNode.id; 100 | resource.setOutput(stateNode.output); 101 | await deleteResource({ resource, state, dryRun }); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/api-gateway/stage.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import * as sdk from "@aws-sdk/client-apigatewayv2"; 3 | import * as z from "zod"; 4 | import { ApiInstance } from "./api"; 5 | import { apiGatewayClient } from "src/utils/aws-clients"; 6 | import { AwsSchema } from "src/utils/types"; 7 | 8 | export type StageSchema = AwsSchema<{ 9 | Key: sdk.DeleteStageRequest; 10 | CreateParams: sdk.CreateStageRequest; 11 | UpdateParams: sdk.UpdateStageRequest; 12 | ReadResult: sdk.GetStageResponse; 13 | }>; 14 | 15 | type StageDependencies = { 16 | api: ApiInstance; 17 | }; 18 | 19 | const stage = resource({ 20 | type: "aws/apiGateway/Stage", 21 | }); 22 | 23 | const stageSchema = stage.defineSchema({ 24 | StageName: { 25 | valueType: z.string(), 26 | propertyType: "param", 27 | presence: "required", 28 | primaryKey: true, 29 | }, 30 | ApiId: { 31 | valueType: z.string(), 32 | propertyType: "param", 33 | presence: "required", 34 | secondaryKey: true, 35 | }, 36 | AccessLogSettings: { 37 | valueType: z.object({ 38 | DestinationArn: z.string().optional(), 39 | Format: z.string().optional(), 40 | }), 41 | propertyType: "param", 42 | presence: "optional", 43 | }, 44 | AutoDeploy: { 45 | valueType: z.boolean(), 46 | propertyType: "param", 47 | presence: "optional", 48 | }, 49 | ClientCertificateId: { 50 | valueType: z.string(), 51 | propertyType: "param", 52 | presence: "optional", 53 | }, 54 | DefaultRouteSettings: { 55 | valueType: z.object({ 56 | DataTraceEnabled: z.boolean().optional(), 57 | DetailedMetricsEnabled: z.boolean().optional(), 58 | LoggingLevel: z.enum(["OFF", "ERROR", "INFO"]).optional(), 59 | ThrottlingBurstLimit: z.number().optional(), 60 | ThrottlingRateLimit: z.number().optional(), 61 | }), 62 | propertyType: "param", 63 | presence: "optional", 64 | }, 65 | DeploymentId: { 66 | valueType: z.string(), 67 | propertyType: "param", 68 | presence: "optional", 69 | }, 70 | Description: { 71 | valueType: z.string(), 72 | propertyType: "param", 73 | presence: "optional", 74 | }, 75 | RouteSettings: { 76 | valueType: z.record( 77 | z.string(), 78 | z.object({ 79 | DataTraceEnabled: z.boolean().optional(), 80 | DetailedMetricsEnabled: z.boolean().optional(), 81 | LoggingLevel: z.enum(["OFF", "ERROR", "INFO"]).optional(), 82 | ThrottlingBurstLimit: z.number().optional(), 83 | ThrottlingRateLimit: z.number().optional(), 84 | }), 85 | ), 86 | propertyType: "param", 87 | presence: "optional", 88 | }, 89 | StageVariables: { 90 | valueType: z.record(z.string()), 91 | propertyType: "param", 92 | presence: "optional", 93 | }, 94 | Tags: { 95 | valueType: z.record(z.string()), 96 | propertyType: "param", 97 | presence: "optional", 98 | immutable: true, 99 | }, 100 | } as const); 101 | 102 | export const Stage = stageSchema 103 | .defineOperations({ 104 | create: async (params) => { 105 | const command = new sdk.CreateStageCommand(params); 106 | await apiGatewayClient.send(command); 107 | }, 108 | read: async (key) => { 109 | const command = new sdk.GetStageCommand(key); 110 | return apiGatewayClient.send(command); 111 | }, 112 | update: async (key, params) => { 113 | const command = new sdk.UpdateStageCommand({ ...key, ...params }); 114 | await apiGatewayClient.send(command); 115 | }, 116 | delete: async (key) => { 117 | const command = new sdk.DeleteStageCommand(key); 118 | await apiGatewayClient.send(command); 119 | }, 120 | }) 121 | .requireDependencies() 122 | .setIntrinsicConfig(({ deps }) => ({ 123 | ApiId: deps.api.output.ApiId!, 124 | })); 125 | 126 | export type StageInstance = InstanceType; 127 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/lambda/lambda-api-permission.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import * as sdk from "@aws-sdk/client-lambda"; 3 | import * as z from "zod"; 4 | import { lambdaClient } from "src/utils/aws-clients"; 5 | import { AwsSchema } from "src/utils/types"; 6 | import { ApiInstance } from "src/resources/api-gateway/api"; 7 | import { generateApiGatewaySourceArn } from "src/templates/arn"; 8 | import { LambdaFunctionInstance } from "./lambda"; 9 | 10 | export type LambdaApiGatewayV2PermissionSchema = AwsSchema<{ 11 | Key: sdk.RemovePermissionRequest; 12 | CreateParams: sdk.AddPermissionRequest; 13 | }>; 14 | 15 | export type LambdaApiGatewayV2PermissionDependencies = { 16 | lambda: LambdaFunctionInstance; 17 | api: ApiInstance; 18 | }; 19 | 20 | const lambdaApiGatewayV2Permission = 21 | resource({ 22 | type: "aws/lambda/LambdaApiGatewayV2Permission", 23 | }); 24 | 25 | const lambdaApiGatewayV2PermissionSchema = 26 | lambdaApiGatewayV2Permission.defineSchema({ 27 | FunctionName: { 28 | valueType: z.string(), 29 | propertyType: "param", 30 | presence: "required", 31 | primaryKey: true, 32 | }, 33 | StatementId: { 34 | valueType: z.string(), 35 | propertyType: "param", 36 | presence: "required", 37 | secondaryKey: true, 38 | }, 39 | Qualifier: { 40 | valueType: z.string(), 41 | propertyType: "param", 42 | presence: "optional", 43 | secondaryKey: true, 44 | }, 45 | RevisionId: { 46 | valueType: z.string(), 47 | propertyType: "param", 48 | presence: "optional", 49 | secondaryKey: true, 50 | }, 51 | Action: { 52 | valueType: z.string(), 53 | propertyType: "param", 54 | presence: "required", 55 | }, 56 | Principal: { 57 | valueType: z.string(), 58 | propertyType: "param", 59 | presence: "required", 60 | }, 61 | FunctionUrlAuthType: { 62 | valueType: z.enum(["NONE", "AWS_IAM"]), 63 | propertyType: "param", 64 | presence: "optional", 65 | }, 66 | InvocationType: { 67 | valueType: z.enum(["Event", "RequestResponse", "DryRun"]), 68 | propertyType: "param", 69 | presence: "optional", 70 | }, 71 | Policy: { 72 | valueType: z.string(), 73 | propertyType: "computed", 74 | presence: "optional", 75 | }, 76 | PrincipalOrgID: { 77 | valueType: z.string(), 78 | propertyType: "param", 79 | presence: "optional", 80 | }, 81 | SourceArn: { 82 | valueType: z.string(), 83 | propertyType: "param", 84 | presence: "optional", 85 | }, 86 | EventSourceToken: { 87 | valueType: z.string(), 88 | propertyType: "param", 89 | presence: "optional", 90 | }, 91 | SourceAccount: { 92 | valueType: z.string(), 93 | propertyType: "param", 94 | presence: "optional", 95 | }, 96 | } as const); 97 | 98 | export const LambdaApiGatewayV2Permission = lambdaApiGatewayV2PermissionSchema 99 | .defineOperations({ 100 | create: async (params) => { 101 | const command = new sdk.AddPermissionCommand(params); 102 | await lambdaClient.send(command); 103 | }, 104 | delete: async (key) => { 105 | const command = new sdk.RemovePermissionCommand(key); 106 | await lambdaClient.send(command); 107 | }, 108 | }) 109 | .requireDependencies() 110 | .setIntrinsicConfig(async ({ deps }) => ({ 111 | FunctionName: deps.lambda.output.FunctionName, 112 | StatementId: "lambda-api-gateway-v2-permission", 113 | Action: "lambda:InvokeFunction", 114 | Principal: "apigateway.amazonaws.com", 115 | SourceArn: await generateApiGatewaySourceArn(deps.api.output.ApiId!), 116 | })); 117 | 118 | export type LambdaApiGatewayV2PermissionInstance = InstanceType< 119 | typeof LambdaApiGatewayV2Permission 120 | >; 121 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/event-bridge/lambda-permission.ts: -------------------------------------------------------------------------------- 1 | import { AwsSchema } from "src/utils/types"; 2 | import * as sdk from "@aws-sdk/client-lambda"; 3 | import { LambdaFunctionInstance } from "../lambda"; 4 | import { EventBridgeRuleInstance } from "./rule"; 5 | import { resource } from "@notation/core"; 6 | import z from "zod"; 7 | import { lambdaClient } from "src/utils/aws-clients"; 8 | 9 | // TODO: much of the lambda permission types can be shared between event-bridge and api-gateway (other than the dependencies part): 10 | // move to a shared module? 11 | 12 | export type LambdaEventBridgeRulePermissionSchema = AwsSchema<{ 13 | Key: sdk.RemovePermissionRequest; 14 | CreateParams: sdk.AddPermissionRequest; 15 | }>; 16 | 17 | export type LambdaEventBridgeRulePermissionDependencies = { 18 | lambda: LambdaFunctionInstance; 19 | eventBridgeRule: EventBridgeRuleInstance; 20 | }; 21 | 22 | const lambdaEventBridgeRulePermission = 23 | resource({ 24 | type: "aws/eventBridge/LambdaEventBridgeRulePermission", 25 | }); 26 | 27 | const LambdaEventBridgeRulePermissionSchema = 28 | lambdaEventBridgeRulePermission.defineSchema({ 29 | FunctionName: { 30 | valueType: z.string(), 31 | propertyType: "param", 32 | presence: "required", 33 | primaryKey: true, 34 | }, 35 | StatementId: { 36 | valueType: z.string(), 37 | propertyType: "param", 38 | presence: "required", 39 | secondaryKey: true, 40 | }, 41 | Qualifier: { 42 | valueType: z.string(), 43 | propertyType: "param", 44 | presence: "optional", 45 | secondaryKey: true, 46 | }, 47 | RevisionId: { 48 | valueType: z.string(), 49 | propertyType: "param", 50 | presence: "optional", 51 | secondaryKey: true, 52 | }, 53 | Action: { 54 | valueType: z.string(), 55 | propertyType: "param", 56 | presence: "required", 57 | }, 58 | Principal: { 59 | valueType: z.string(), 60 | propertyType: "param", 61 | presence: "required", 62 | }, 63 | FunctionUrlAuthType: { 64 | valueType: z.enum(["NONE", "AWS_IAM"]), 65 | propertyType: "param", 66 | presence: "optional", 67 | }, 68 | InvocationType: { 69 | valueType: z.enum(["Event", "RequestResponse", "DryRun"]), 70 | propertyType: "param", 71 | presence: "optional", 72 | }, 73 | Policy: { 74 | valueType: z.string(), 75 | propertyType: "computed", 76 | presence: "optional", 77 | }, 78 | PrincipalOrgID: { 79 | valueType: z.string(), 80 | propertyType: "param", 81 | presence: "optional", 82 | }, 83 | SourceArn: { 84 | valueType: z.string(), 85 | propertyType: "param", 86 | presence: "optional", 87 | }, 88 | EventSourceToken: { 89 | valueType: z.string(), 90 | propertyType: "param", 91 | presence: "optional", 92 | }, 93 | SourceAccount: { 94 | valueType: z.string(), 95 | propertyType: "param", 96 | presence: "optional", 97 | }, 98 | // Todo: why does this solve the compile error at the top level? 99 | } as const); 100 | 101 | export const LambdaEventBridgeRulePermission = 102 | LambdaEventBridgeRulePermissionSchema.defineOperations({ 103 | create: async (params) => { 104 | const command = new sdk.AddPermissionCommand(params); 105 | await lambdaClient.send(command); 106 | }, 107 | delete: async (key) => { 108 | const command = new sdk.RemovePermissionCommand(key); 109 | await lambdaClient.send(command); 110 | }, 111 | }) 112 | .requireDependencies() 113 | .setIntrinsicConfig(async ({ deps }) => { 114 | return { 115 | FunctionName: deps.lambda.output.FunctionName, 116 | StatementId: `LambdaEventBridgeRulePermission-${deps.lambda.output.FunctionName}`, 117 | Action: "lambda:InvokeFunction", 118 | Principal: "events.amazonaws.com", 119 | SourceArn: deps.eventBridgeRule.output.Arn, 120 | }; 121 | }); 122 | 123 | export type LambdaEventBridgeRulePermissionInstance = InstanceType< 124 | typeof LambdaEventBridgeRulePermission 125 | >; 126 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/api-gateway/api.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import * as z from "zod"; 3 | import * as sdk from "@aws-sdk/client-apigatewayv2"; 4 | import { apiGatewayClient } from "src/utils/aws-clients"; 5 | import { AwsSchema } from "src/utils/types"; 6 | 7 | type ApiSdkSchema = AwsSchema<{ 8 | Key: sdk.DeleteApiRequest; 9 | CreateParams: sdk.CreateApiRequest; 10 | UpdateParams: sdk.UpdateApiRequest; 11 | ReadResult: sdk.GetApiResponse; 12 | }>; 13 | 14 | const api = resource({ 15 | type: "aws/apiGateway/Api", 16 | }); 17 | 18 | const apiSchema = api.defineSchema({ 19 | ApiId: { 20 | propertyType: "computed", 21 | valueType: z.string(), 22 | presence: "required", 23 | primaryKey: true, 24 | }, 25 | ApiEndpoint: { 26 | propertyType: "computed", 27 | valueType: z.string(), 28 | presence: "required", 29 | }, 30 | ApiGatewayManaged: { 31 | propertyType: "computed", 32 | valueType: z.boolean(), 33 | presence: "optional", 34 | }, 35 | ApiKeySelectionExpression: { 36 | propertyType: "param", 37 | valueType: z.string(), 38 | presence: "optional", 39 | }, 40 | CorsConfiguration: { 41 | propertyType: "param", 42 | valueType: z.object({ 43 | AllowCredentials: z.boolean().optional(), 44 | AllowHeaders: z.array(z.string()).optional(), 45 | AllowMethods: z.array(z.string()).optional(), 46 | AllowOrigins: z.array(z.string()).optional(), 47 | ExposeHeaders: z.array(z.string()).optional(), 48 | MaxAge: z.number().optional(), 49 | }), 50 | presence: "optional", 51 | }, 52 | CreatedDate: { 53 | propertyType: "computed", 54 | valueType: z.date(), 55 | presence: "required", 56 | volatile: true, 57 | }, 58 | Description: { 59 | propertyType: "param", 60 | valueType: z.string(), 61 | presence: "optional", 62 | }, 63 | DisableExecuteApiEndpoint: { 64 | propertyType: "param", 65 | valueType: z.boolean(), 66 | presence: "optional", 67 | }, 68 | DisableSchemaValidation: { 69 | propertyType: "param", 70 | valueType: z.boolean(), 71 | presence: "optional", 72 | }, 73 | ImportInfo: { 74 | propertyType: "computed", 75 | valueType: z.array(z.string()), 76 | presence: "optional", 77 | }, 78 | Name: { 79 | propertyType: "param", 80 | valueType: z.string(), 81 | presence: "required", 82 | }, 83 | ProtocolType: { 84 | propertyType: "param", 85 | valueType: z.enum(["HTTP", "WEBSOCKET"]), 86 | defaultValue: "HTTP", 87 | immutable: true, 88 | presence: "required", 89 | }, 90 | RouteKey: { 91 | propertyType: "param", 92 | valueType: z.string(), 93 | presence: "optional", 94 | }, 95 | RouteSelectionExpression: { 96 | propertyType: "param", 97 | valueType: z.string(), 98 | presence: "optional", 99 | }, 100 | Tags: { 101 | propertyType: "param", 102 | valueType: z.record(z.string()), 103 | presence: "optional", 104 | immutable: true, 105 | }, 106 | Warnings: { 107 | propertyType: "computed", 108 | valueType: z.array(z.string()), 109 | presence: "optional", 110 | }, 111 | Version: { 112 | propertyType: "param", 113 | valueType: z.string(), 114 | presence: "optional", 115 | }, 116 | } as const); 117 | 118 | export const Api = apiSchema.defineOperations({ 119 | async create(params) { 120 | const command = new sdk.CreateApiCommand(params); 121 | const result = await apiGatewayClient.send(command); 122 | return { ApiId: result.ApiId! }; 123 | }, 124 | async read(key) { 125 | const command = new sdk.GetApiCommand(key); 126 | const result = await apiGatewayClient.send(command); 127 | // todo: check types or correct or if RouteKey is actually in result 128 | // if not, need to pass the original params to read 129 | return { RouteKey: "", ...result }; 130 | }, 131 | async update(key, params) { 132 | const command = new sdk.UpdateApiCommand({ ...key, ...params }); 133 | await apiGatewayClient.send(command); 134 | }, 135 | async delete(pk) { 136 | const command = new sdk.DeleteApiCommand(pk); 137 | await apiGatewayClient.send(command); 138 | }, 139 | }); 140 | 141 | export type ApiInstance = InstanceType; 142 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/api-gateway/route.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import * as z from "zod"; 3 | import * as sdk from "@aws-sdk/client-apigatewayv2"; 4 | import { apiGatewayClient } from "src/utils/aws-clients"; 5 | import { ApiInstance, LambdaIntegrationInstance } from "."; 6 | import { AwsSchema } from "src/utils/types"; 7 | import { AuthInstance } from "./auth"; 8 | 9 | type RouteSdkSchema = AwsSchema<{ 10 | Key: sdk.DeleteRouteRequest; 11 | CreateParams: sdk.CreateRouteRequest; 12 | UpdateParams: sdk.UpdateRouteRequest; 13 | ReadResult: sdk.GetRouteResult; 14 | }>; 15 | 16 | type RouteDependencies = { 17 | api: ApiInstance; 18 | lambdaIntegration: LambdaIntegrationInstance; 19 | auth?: AuthInstance; 20 | }; 21 | 22 | const route = resource({ 23 | type: "aws/apiGateway/Route", 24 | }); 25 | 26 | export const routeSchema = route.defineSchema({ 27 | RouteId: { 28 | valueType: z.string(), 29 | propertyType: "computed", 30 | presence: "required", 31 | primaryKey: true, 32 | }, 33 | ApiId: { 34 | valueType: z.string(), 35 | propertyType: "param", 36 | presence: "required", 37 | secondaryKey: true, 38 | }, 39 | ApiKeyRequired: { 40 | valueType: z.boolean(), 41 | propertyType: "param", 42 | presence: "optional", 43 | }, 44 | ApiGatewayManaged: { 45 | valueType: z.boolean(), 46 | propertyType: "computed", 47 | presence: "optional", 48 | }, 49 | AuthorizationScopes: { 50 | valueType: z.array(z.string()), 51 | propertyType: "param", 52 | presence: "optional", 53 | }, 54 | AuthorizationType: { 55 | valueType: z.enum(["NONE", "AWS_IAM", "CUSTOM", "JWT"]), 56 | propertyType: "param", 57 | presence: "optional", 58 | }, 59 | AuthorizerId: { 60 | valueType: z.string(), 61 | propertyType: "param", 62 | presence: "optional", 63 | }, 64 | ModelSelectionExpression: { 65 | valueType: z.string(), 66 | propertyType: "param", 67 | presence: "optional", 68 | }, 69 | OperationName: { 70 | valueType: z.string(), 71 | propertyType: "param", 72 | presence: "optional", 73 | }, 74 | RequestModels: { 75 | valueType: z.record(z.string()), 76 | propertyType: "param", 77 | presence: "optional", 78 | }, 79 | RequestParameters: { 80 | valueType: z.record( 81 | z.object({ 82 | Required: z.boolean().optional(), 83 | }), 84 | ), 85 | propertyType: "param", 86 | presence: "optional", 87 | }, 88 | RouteKey: { 89 | valueType: z.string(), 90 | propertyType: "param", 91 | presence: "required", 92 | }, 93 | RouteResponseSelectionExpression: { 94 | valueType: z.string(), 95 | propertyType: "param", 96 | presence: "optional", 97 | }, 98 | Target: { 99 | valueType: z.string(), 100 | propertyType: "param", 101 | presence: "optional", 102 | }, 103 | } as const); 104 | 105 | export const Route = routeSchema 106 | .defineOperations({ 107 | create: async (params) => { 108 | const command = new sdk.CreateRouteCommand(params); 109 | const result = await apiGatewayClient.send(command); 110 | 111 | return { RouteId: result.RouteId! }; 112 | }, 113 | read: async (key) => { 114 | const command = new sdk.GetRouteCommand(key); 115 | const result = await apiGatewayClient.send(command); 116 | return { ...key, ...result }; 117 | }, 118 | update: async (key, patch, params) => { 119 | const command = new sdk.UpdateRouteCommand({ ...key, ...params }); 120 | await apiGatewayClient.send(command); 121 | }, 122 | delete: async (key) => { 123 | const command = new sdk.DeleteRouteCommand(key); 124 | await apiGatewayClient.send(command); 125 | }, 126 | }) 127 | .requireDependencies() 128 | .setIntrinsicConfig(({ deps }) => { 129 | const authConfig = deps.auth 130 | ? { AuthorizerId: deps.auth.output.AuthorizerId } 131 | : {}; 132 | 133 | return { 134 | ApiId: deps.api.output.ApiId, 135 | // todo: this is too opinionated, should be somewhere else 136 | Target: `integrations/${deps.lambdaIntegration.output.IntegrationId}`, 137 | ...authConfig, 138 | }; 139 | }); 140 | 141 | export type RouteInstance = InstanceType; 142 | -------------------------------------------------------------------------------- /packages/core/src/orchestrator/resource.schema.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod"; 2 | import { FallbackIf } from "src/utils/types"; 3 | 4 | export type Schema = Record>; 5 | 6 | export type SchemaItem = { 7 | valueType: z.ZodType; 8 | presence: "required" | "optional"; 9 | sensitive?: true; 10 | hidden?: true; 11 | volatile?: true; 12 | } & ( 13 | | { 14 | propertyType: "param"; 15 | immutable?: true; 16 | defaultValue?: T; 17 | primaryKey?: true; 18 | secondaryKey?: true; 19 | } 20 | | { 21 | propertyType: "computed"; 22 | primaryKey?: true; 23 | } 24 | | { 25 | propertyType: "derived"; 26 | } 27 | ); 28 | 29 | /** 30 | * Extracts the primary key type from a schema 31 | */ 32 | export type ComputedPrimaryKey = FallbackIf< 33 | MapSchema, 34 | {}, 35 | void 36 | >; 37 | 38 | /** 39 | * Extract the compound key of a resource from a schema 40 | */ 41 | export type CompoundKey = MapSchema< 42 | S, 43 | never, 44 | "primaryKey" | "secondaryKey" 45 | >; 46 | 47 | /** 48 | * Extract the write params of a resource from a schema 49 | */ 50 | export type Params = MapSchema< 51 | S, 52 | { propertyType: "computed" | "derived" } 53 | >; 54 | 55 | /** 56 | * Extract the read result of a resource from a schema 57 | * Partial as it's pretty tricky to know what will come back from a read 58 | */ 59 | export type Result = DeepPartial< 60 | MapSchema 61 | >; 62 | 63 | /** 64 | * Maps the schema to the output type, containing all properties 65 | */ 66 | export type Output = MapSchema; 67 | 68 | /** 69 | * Maps the schema to the state type, omitting hidden properties 70 | */ 71 | export type State = MapSchema; 72 | 73 | /** 74 | * Produces a schema type that is constrained by an API's types 75 | */ 76 | export type SchemaFromApi< 77 | ApiCompoundKey, 78 | ApiCreateParams, 79 | ApiUpdateParams, 80 | ApiReadResult, 81 | > = { 82 | // todo: infer propertyType too 83 | [K in keyof ApiCompoundKey]: SchemaItem & 84 | ({ primaryKey: true } | { secondaryKey: true }); 85 | } & { 86 | [K in keyof OmitOptional< 87 | Omit 88 | >]: SchemaItem & { 89 | propertyType: "param"; 90 | presence: "required"; 91 | }; 92 | } & { 93 | [K in keyof PickOptional< 94 | Omit 95 | >]: SchemaItem & { 96 | propertyType: "param"; 97 | presence: "optional"; 98 | }; 99 | } & { 100 | [K in keyof Omit]: SchemaItem< 101 | ApiCreateParams[K] 102 | > & { 103 | propertyType: "param"; 104 | immutable: true; 105 | }; 106 | } & { 107 | [K in keyof Omit< 108 | ApiReadResult, 109 | keyof ApiCreateParams | keyof ApiCompoundKey 110 | >]: SchemaItem & { 111 | propertyType: "computed"; 112 | }; 113 | }; 114 | 115 | /** 116 | * Maps zod `valueType` to the output type. 117 | * Makes optional fields optional. 118 | * Excludes properties matching the `ExcludeConditions` type. 119 | */ 120 | type MapSchema< 121 | S extends Schema, 122 | ExcludeConditions = never, 123 | ExcludeKey = any, 124 | > = S extends { 125 | [K in keyof S]: { valueType: any }; 126 | } 127 | ? Intersect< 128 | { 129 | [K in keyof S as S[K] extends 130 | | { presence: "optional" } 131 | | ExcludeConditions 132 | ? never 133 | : ExcludeKey extends keyof S[K] 134 | ? K 135 | : never]: S[K]["valueType"]["_output"]; 136 | }, 137 | { 138 | [K in keyof S as S[K] extends { presence: "optional" } 139 | ? S[K] extends ExcludeConditions 140 | ? never 141 | : ExcludeKey extends keyof S[K] 142 | ? K 143 | : never 144 | : never]?: S[K]["valueType"]["_output"]; 145 | } 146 | > 147 | : never; 148 | 149 | /** 150 | * Unpack an intersection 151 | */ 152 | type Intersect = [T] extends [U] ? T : [U] extends [T] ? U : T & U; 153 | 154 | /** 155 | * Get non-optional properties from a type. 156 | */ 157 | type OptionalKeys = { 158 | [K in keyof T]: undefined extends T[K] ? never : K; 159 | }[keyof T]; 160 | 161 | type OmitOptional = Pick>; 162 | type PickOptional = Omit>; 163 | 164 | type DeepPartial = { 165 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 166 | }; 167 | -------------------------------------------------------------------------------- /packages/core/test/orchestrator/resource.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it, test, vi } from "vitest"; 2 | import { resource } from "src"; 3 | import { 4 | TestResource, 5 | testResourceConfig, 6 | testResourceOutput, 7 | } from "./resource.doubles"; 8 | import { describe } from "node:test"; 9 | 10 | describe("resource basics", () => { 11 | const TestResource = resource({ type: "provider/service/resource" }) 12 | .defineSchema({}) 13 | .defineOperations({} as any); 14 | 15 | const testResource = new TestResource({ id: "0" }); 16 | 17 | test("type", () => { 18 | expect(TestResource.type).toBe("provider/service/resource"); 19 | expect(testResource.type).toBe("provider/service/resource"); 20 | }); 21 | 22 | test("meta", () => { 23 | const expectedMeta = { 24 | moduleName: "@notation/provider.iac", 25 | serviceName: "service", 26 | resourceName: "resource", 27 | }; 28 | expect(testResource.meta).toEqual(expectedMeta); 29 | }); 30 | 31 | test("setOutput", () => { 32 | testResource.setOutput({ a: 1 }); 33 | expect(testResource.output).toEqual({ a: 1 }); 34 | }); 35 | }); 36 | 37 | describe("resource schema", () => { 38 | const testResource = new TestResource({ 39 | id: "test-resource-1", 40 | config: testResourceConfig, 41 | }); 42 | 43 | test("key (primary)", () => { 44 | testResource.setOutput(testResourceOutput); 45 | expect(testResource.key).toEqual({ primaryKey: "0" }); 46 | }); 47 | 48 | test("key (compound)", () => { 49 | testResource.setOutput({ 50 | ...testResourceOutput, 51 | optionalSecondaryKey: 1, 52 | }); 53 | expect(testResource.key).toEqual({ 54 | primaryKey: "0", 55 | optionalSecondaryKey: 1, 56 | }); 57 | }); 58 | 59 | test("toComparable removes computed, hidden and volatile fields", () => { 60 | testResource.setOutput(testResourceOutput); 61 | const comparableState = testResource.toComparable(testResourceOutput); 62 | const { primaryKey, hiddenParam, volatileComputed, ...expectedState } = 63 | testResourceOutput; 64 | expect(comparableState).toEqual(expectedState); 65 | }); 66 | 67 | test("toState removes hidden params", () => { 68 | testResource.setOutput(testResourceOutput); 69 | const state = testResource.toState(testResourceOutput); 70 | const { hiddenParam, ...expectedState } = testResourceOutput; 71 | expect(state).toEqual(expectedState); 72 | }); 73 | 74 | test("getParams merges intrinsic config", () => { 75 | [ 76 | { requiredParam: "name1", hiddenParam: "" }, 77 | { requiredParam: "name1", hiddenParam: "", optionalSecondaryKey: 1 }, 78 | ].forEach(async (params) => { 79 | const testResource = new TestResource({ 80 | id: "test-resource-1", 81 | config: params, 82 | }); 83 | expect(await testResource.getParams()).toEqual({ 84 | ...params, 85 | intrinsicParam: true, 86 | }); 87 | }); 88 | }); 89 | }); 90 | 91 | describe("resource dependencies", () => { 92 | it("passes dependencies to getIntrinsicConfig", async () => { 93 | const getIntrinsicConfigMock = vi.fn(); 94 | 95 | const TestResourceWithDeps = TestResource.requireDependencies<{ 96 | dep1: InstanceType; 97 | }>().setIntrinsicConfig(getIntrinsicConfigMock); 98 | 99 | const childTestResource = new TestResource({ 100 | id: "test-resource-1", 101 | config: testResourceConfig, 102 | }); 103 | 104 | const testResource = new TestResourceWithDeps({ 105 | id: "test-resource-1", 106 | config: testResourceConfig, 107 | dependencies: { dep1: childTestResource }, 108 | }); 109 | 110 | await testResource.getParams(); 111 | 112 | expect(getIntrinsicConfigMock.mock.calls[0]).toEqual([ 113 | { 114 | id: "test-resource-1", 115 | config: testResourceConfig, 116 | deps: { dep1: childTestResource }, 117 | }, 118 | ]); 119 | }); 120 | 121 | it("merges config and intrinsic config", async () => { 122 | const getIntrinsicConfigMock = vi.fn(() => ({ 123 | requiredParam: "preset", 124 | })); 125 | 126 | const { requiredParam, ...nonIntrinsicConfig } = testResourceConfig; 127 | 128 | const TestResourceWithDeps = TestResource.requireDependencies<{ 129 | dep1: InstanceType; 130 | }>().setIntrinsicConfig(getIntrinsicConfigMock); 131 | 132 | const childTestResource = new TestResource({ 133 | id: "test-resource-1", 134 | config: testResourceConfig, 135 | }); 136 | 137 | const testResource = new TestResourceWithDeps({ 138 | id: "test-resource-1", 139 | config: nonIntrinsicConfig, 140 | dependencies: { dep1: childTestResource }, 141 | }); 142 | 143 | expect(await testResource.getParams()).toEqual({ 144 | ...testResourceConfig, 145 | requiredParam: "preset", 146 | intrinsicParam: true, 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /packages/aws.iac/src/resources/event-bridge/rule.ts: -------------------------------------------------------------------------------- 1 | import { resource } from "@notation/core"; 2 | import { AwsSchema } from "src/utils/types"; 3 | import * as sdk from "@aws-sdk/client-eventbridge"; 4 | import z from "zod"; 5 | import { eventBridgeClient } from "src/utils/aws-clients"; 6 | import { LambdaFunctionInstance } from "../lambda"; 7 | 8 | export type EventBridgeRuleSchema = AwsSchema<{ 9 | Key: sdk.DescribeRuleCommandInput; 10 | // We omit the 'Rule' field since this is just an alias of the 'Name' field 11 | CreateParams: sdk.PutRuleCommandInput & 12 | Omit; 13 | UpdateParams: sdk.PutRuleCommandInput & 14 | Omit; 15 | ReadResult: Omit< 16 | sdk.DescribeRuleCommandOutput & sdk.ListTargetsByRuleCommandOutput, 17 | "$metadata" 18 | >; 19 | }>; 20 | 21 | export type EventBridgeRuleDependencies = { 22 | lambda: LambdaFunctionInstance; 23 | }; 24 | 25 | const eventBridgeRule = resource({ 26 | type: "aws/eventBridge/rule", 27 | }); 28 | 29 | const targetSchema = z.object({ 30 | Id: z.string(), 31 | Arn: z.string(), 32 | }); 33 | 34 | const eventBridgeRuleSchema = eventBridgeRule.defineSchema({ 35 | Name: { 36 | valueType: z.string(), 37 | presence: "required", 38 | primaryKey: true, 39 | propertyType: "param", 40 | }, 41 | EventBusName: { 42 | valueType: z.string(), 43 | presence: "required", 44 | defaultValue: "default", 45 | propertyType: "param", 46 | secondaryKey: true, 47 | }, 48 | ScheduleExpression: { 49 | valueType: z.string().optional(), 50 | presence: "optional", 51 | propertyType: "param", 52 | }, 53 | EventPattern: { 54 | // Note: this is a JSON string representation of an event pattern: 55 | // (https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-patterns.html#eb-filtering-data-types) 56 | valueType: z.string().optional(), 57 | presence: "optional", 58 | propertyType: "param", 59 | }, 60 | Targets: { 61 | presence: "required", 62 | propertyType: "param", 63 | valueType: z.array(targetSchema), 64 | }, 65 | Arn: { 66 | presence: "required", 67 | propertyType: "computed", 68 | valueType: z.string(), 69 | }, 70 | }); 71 | 72 | export const EventBridgeRule = eventBridgeRuleSchema 73 | .defineOperations({ 74 | read: async (key) => { 75 | const describeRuleCommand = new sdk.DescribeRuleCommand(key); 76 | const listRuleTargetsCommand = new sdk.ListTargetsByRuleCommand({ 77 | Rule: key.Name, 78 | EventBusName: key.EventBusName, 79 | }); 80 | 81 | const [ruleDescriptionResult, listRuleTargetsResult] = await Promise.all([ 82 | eventBridgeClient.send(describeRuleCommand), 83 | eventBridgeClient.send(listRuleTargetsCommand), 84 | ]); 85 | 86 | return { 87 | ...ruleDescriptionResult, 88 | ...listRuleTargetsResult, 89 | }; 90 | }, 91 | 92 | create: async (params) => { 93 | const createRuleCommand = new sdk.PutRuleCommand(params); 94 | const createTargetsCommand = new sdk.PutTargetsCommand({ 95 | Rule: params.Name, 96 | EventBusName: params.EventBusName, 97 | Targets: params.Targets, 98 | }); 99 | 100 | await eventBridgeClient.send(createRuleCommand); 101 | await eventBridgeClient.send(createTargetsCommand); 102 | }, 103 | update: async (key, patch, params) => { 104 | const { Targets, ...ruleConfig } = patch; 105 | 106 | const updateRuleCommand = new sdk.PutRuleCommand(ruleConfig); 107 | const updateTargetsCommand = new sdk.PutTargetsCommand({ 108 | Rule: ruleConfig.Name, 109 | EventBusName: ruleConfig.EventBusName, 110 | Targets: Targets, 111 | }); 112 | 113 | await eventBridgeClient.send(updateRuleCommand); 114 | await eventBridgeClient.send(updateTargetsCommand); 115 | }, 116 | delete: async (key) => { 117 | // The targets must be deleted first, otherwise the API will return an error response 118 | 119 | const existingTargets = await eventBridgeClient.send( 120 | new sdk.ListTargetsByRuleCommand({ 121 | Rule: key.Name, 122 | EventBusName: key.EventBusName, 123 | }), 124 | ); 125 | 126 | if (existingTargets.Targets && existingTargets.Targets.length > 0) { 127 | const deleteTargetsCommand = new sdk.RemoveTargetsCommand({ 128 | Rule: key.Name, 129 | EventBusName: key.EventBusName, 130 | Ids: existingTargets.Targets.map((target) => target.Id!), 131 | }); 132 | 133 | await eventBridgeClient.send(deleteTargetsCommand); 134 | } 135 | 136 | const deleteRuleCommand = new sdk.DeleteRuleCommand(key); 137 | await eventBridgeClient.send(deleteRuleCommand); 138 | }, 139 | }) 140 | .requireDependencies() 141 | .setIntrinsicConfig(async ({ deps }) => ({ 142 | Targets: [ 143 | { 144 | Id: deps.lambda.output.FunctionName, 145 | Arn: deps.lambda.output.FunctionArn, 146 | }, 147 | ], 148 | })); 149 | 150 | export type EventBridgeRuleInstance = InstanceType; 151 | --------------------------------------------------------------------------------