├── .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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------