effortless-aws 0.0.2 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # effortless-aws
2
+
3
+ Code-first AWS Lambda framework. Export handlers, deploy to AWS. No infrastructure files needed.
4
+
5
+ ```bash
6
+ npm install effortless-aws
7
+ ```
8
+
9
+ ## What it looks like
10
+
11
+ ```typescript
12
+ // src/api.ts
13
+ import { defineHttp, defineTable, param } from "effortless-aws";
14
+
15
+ // DynamoDB table — just export it, get the table
16
+ export const orders = defineTable<Order>({
17
+ pk: { name: "id", type: "string" },
18
+ onRecord: async ({ record, table }) => {
19
+ if (record.eventName === "INSERT") {
20
+ await table.put({ ...record.new!, status: "confirmed" });
21
+ }
22
+ },
23
+ });
24
+
25
+ // HTTP endpoint — creates API Gateway + Lambda + route
26
+ export const createOrder = defineHttp({
27
+ method: "POST",
28
+ path: "/orders",
29
+ schema: (input) => parseOrder(input),
30
+ deps: { orders },
31
+ params: { apiKey: param("stripe-key") },
32
+ context: async ({ params }) => ({
33
+ stripe: new Stripe(params.apiKey),
34
+ }),
35
+ onRequest: async ({ data, ctx, deps }) => {
36
+ await deps.orders.put({ id: crypto.randomUUID(), ...data });
37
+ return { status: 201, body: { ok: true } };
38
+ },
39
+ });
40
+ ```
41
+
42
+ ```bash
43
+ npx eff deploy
44
+ ```
45
+
46
+ That's it. No YAML, no CloudFormation, no state files.
47
+
48
+ ## Why
49
+
50
+ Traditional Lambda development splits infrastructure and code across multiple files and languages. Adding a single endpoint means touching CloudFormation/CDK/Terraform templates, IAM policies, and handler code separately.
51
+
52
+ **Effortless** derives infrastructure from your TypeScript exports. One `defineHttp` call creates the API Gateway route, Lambda function, and IAM role. One `defineTable` call creates the DynamoDB table, stream, event source mapping, and processor Lambda.
53
+
54
+ ## Killer features
55
+
56
+ **Infrastructure from code** — export a handler, get the AWS resources. No config files, no YAML.
57
+
58
+ **Typed everything** — `defineTable` schema gives you typed `table.put()`, typed `deps.orders.get()`, typed `record.new`. One definition, types flow everywhere.
59
+
60
+ **Direct AWS SDK deploys** — no CloudFormation, no Pulumi. Direct API calls. Deploy in ~5-10s, not 5-10 minutes.
61
+
62
+ **No state files** — AWS resource tags are the source of truth. No tfstate, no S3 backends, no drift.
63
+
64
+ **Cross-handler deps** — `deps: { orders }` auto-wires IAM permissions and injects a typed `TableClient`. Zero config.
65
+
66
+ **SSM params** — `param("stripe-key")` fetches from Parameter Store at cold start. Auto IAM, auto caching, supports transforms.
67
+
68
+ **Partial batch failures** — DynamoDB stream processing reports failed records individually. No batch-level retries for one bad record.
69
+
70
+ **Cold start caching** — `context` factory runs once per cold start, cached across invocations. Put DB connections, SDK clients, config there.
71
+
72
+ ## Handler types
73
+
74
+ ### HTTP
75
+
76
+ ```typescript
77
+ export const getUser = defineHttp({
78
+ method: "GET",
79
+ path: "/users/{id}",
80
+ onRequest: async ({ req }) => {
81
+ return { status: 200, body: { id: req.params.id } };
82
+ },
83
+ });
84
+ ```
85
+
86
+ ### DynamoDB Table + Stream
87
+
88
+ ```typescript
89
+ export const users = defineTable({
90
+ pk: { name: "id", type: "string" },
91
+ sk: { name: "email", type: "string" },
92
+ ttlAttribute: "expiresAt",
93
+ onRecord: async ({ record, table }) => {
94
+ console.log(record.eventName, record.new);
95
+ },
96
+ });
97
+ ```
98
+
99
+ ### Table (resource only, no stream)
100
+
101
+ ```typescript
102
+ export const sessions = defineTable({
103
+ pk: { name: "id", type: "string" },
104
+ ttlAttribute: "expiresAt",
105
+ });
106
+ ```
107
+
108
+ ## Configuration
109
+
110
+ ```typescript
111
+ // effortless.config.ts
112
+ import { defineConfig } from "effortless-aws";
113
+
114
+ export default defineConfig({
115
+ name: "my-app",
116
+ region: "eu-central-1",
117
+ handlers: ["src/**/*.ts"],
118
+ });
119
+ ```
120
+
121
+ ## CLI
122
+
123
+ ```bash
124
+ npx eff deploy # deploy all handlers
125
+ npx eff deploy --stage prod # deploy to specific stage
126
+ npx eff deploy --only users # deploy single handler
127
+ npx eff destroy # remove all resources
128
+ npx eff logs users --follow # stream CloudWatch logs
129
+ npx eff list # show deployed resources
130
+ ```
131
+
132
+ ## How it works
133
+
134
+ 1. **Static analysis** (ts-morph) — reads your exports, extracts handler config from AST
135
+ 2. **Bundle** (esbuild) — wraps each handler with a runtime adapter
136
+ 3. **Deploy** (AWS SDK) — creates/updates Lambda, API Gateway, DynamoDB, IAM directly
137
+
138
+ No CloudFormation stacks. No Terraform state. Tags on AWS resources are the only state.
139
+
140
+ ## Compared to
141
+
142
+ | | SST v3 | Nitric | Serverless | **Effortless** |
143
+ |---|---|---|---|---|
144
+ | Infra from code (not config) | No | Yes | No | **Yes** |
145
+ | Typed client from schema | No | No | No | **Yes** |
146
+ | No state files | No | No | No | **Yes** |
147
+ | Deploy speed | ~30s | ~30s | minutes | **~5-10s** |
148
+ | Runs in your AWS account | Yes | Yes | Yes | **Yes** |
149
+ | Open source | Yes | Yes | Yes | **Yes** |
150
+
151
+ ## License
152
+
153
+ MIT
@@ -0,0 +1,163 @@
1
+ // src/runtime/table-client.ts
2
+ import { DynamoDB } from "@aws-sdk/client-dynamodb";
3
+ import { marshall, unmarshall } from "@aws-sdk/util-dynamodb";
4
+ var createTableClient = (tableName) => {
5
+ let client2 = null;
6
+ const getClient2 = () => client2 ??= new DynamoDB({});
7
+ return {
8
+ tableName,
9
+ async put(item) {
10
+ await getClient2().putItem({
11
+ TableName: tableName,
12
+ Item: marshall(item, { removeUndefinedValues: true })
13
+ });
14
+ },
15
+ async get(key) {
16
+ const result = await getClient2().getItem({
17
+ TableName: tableName,
18
+ Key: marshall(key, { removeUndefinedValues: true })
19
+ });
20
+ return result.Item ? unmarshall(result.Item) : void 0;
21
+ },
22
+ async delete(key) {
23
+ await getClient2().deleteItem({
24
+ TableName: tableName,
25
+ Key: marshall(key, { removeUndefinedValues: true })
26
+ });
27
+ },
28
+ async update(key, actions) {
29
+ const names = {};
30
+ const values = {};
31
+ const setClauses = [];
32
+ const removeClauses = [];
33
+ let counter = 0;
34
+ if (actions.set) {
35
+ for (const [attr, val] of Object.entries(actions.set)) {
36
+ const alias = `#a${counter}`;
37
+ const valAlias = `:v${counter}`;
38
+ names[alias] = attr;
39
+ values[valAlias] = val;
40
+ setClauses.push(`${alias} = ${valAlias}`);
41
+ counter++;
42
+ }
43
+ }
44
+ if (actions.append) {
45
+ for (const [attr, val] of Object.entries(actions.append)) {
46
+ const alias = `#a${counter}`;
47
+ const valAlias = `:v${counter}`;
48
+ const emptyAlias = `:empty${counter}`;
49
+ names[alias] = attr;
50
+ values[valAlias] = val;
51
+ values[emptyAlias] = [];
52
+ setClauses.push(`${alias} = list_append(if_not_exists(${alias}, ${emptyAlias}), ${valAlias})`);
53
+ counter++;
54
+ }
55
+ }
56
+ if (actions.remove) {
57
+ for (const attr of actions.remove) {
58
+ const alias = `#a${counter}`;
59
+ names[alias] = attr;
60
+ removeClauses.push(alias);
61
+ counter++;
62
+ }
63
+ }
64
+ const parts = [];
65
+ if (setClauses.length) parts.push(`SET ${setClauses.join(", ")}`);
66
+ if (removeClauses.length) parts.push(`REMOVE ${removeClauses.join(", ")}`);
67
+ if (!parts.length) return;
68
+ await getClient2().updateItem({
69
+ TableName: tableName,
70
+ Key: marshall(key, { removeUndefinedValues: true }),
71
+ UpdateExpression: parts.join(" "),
72
+ ExpressionAttributeNames: names,
73
+ ...Object.keys(values).length ? { ExpressionAttributeValues: marshall(values, { removeUndefinedValues: true }) } : {}
74
+ });
75
+ },
76
+ async query(params) {
77
+ const names = { "#pk": params.pk.name };
78
+ const values = { ":pk": params.pk.value };
79
+ let keyCondition = "#pk = :pk";
80
+ if (params.sk) {
81
+ names["#sk"] = params.sk.name;
82
+ values[":sk"] = params.sk.value;
83
+ if (params.sk.condition === "begins_with") {
84
+ keyCondition += " AND begins_with(#sk, :sk)";
85
+ } else {
86
+ keyCondition += ` AND #sk ${params.sk.condition} :sk`;
87
+ }
88
+ }
89
+ const result = await getClient2().query({
90
+ TableName: tableName,
91
+ KeyConditionExpression: keyCondition,
92
+ ExpressionAttributeNames: names,
93
+ ExpressionAttributeValues: marshall(values, { removeUndefinedValues: true }),
94
+ ...params.limit ? { Limit: params.limit } : {},
95
+ ...params.scanIndexForward !== void 0 ? { ScanIndexForward: params.scanIndexForward } : {}
96
+ });
97
+ return (result.Items ?? []).map((item) => unmarshall(item));
98
+ }
99
+ };
100
+ };
101
+
102
+ // src/runtime/ssm-client.ts
103
+ import { SSM } from "@aws-sdk/client-ssm";
104
+ var client = null;
105
+ var getClient = () => client ??= new SSM({});
106
+ var getParameters = async (names) => {
107
+ const map = /* @__PURE__ */ new Map();
108
+ for (let i = 0; i < names.length; i += 10) {
109
+ const batch = names.slice(i, i + 10);
110
+ const result = await getClient().getParameters({
111
+ Names: batch,
112
+ WithDecryption: true
113
+ });
114
+ for (const p of result.Parameters ?? []) {
115
+ if (p.Name && p.Value !== void 0) {
116
+ map.set(p.Name, p.Value);
117
+ }
118
+ }
119
+ }
120
+ return map;
121
+ };
122
+
123
+ // src/runtime/handler-utils.ts
124
+ var ENV_TABLE_PREFIX = "EFF_TABLE_";
125
+ var ENV_PARAM_PREFIX = "EFF_PARAM_";
126
+ var buildDeps = (deps) => {
127
+ if (!deps) return void 0;
128
+ const result = {};
129
+ for (const key of Object.keys(deps)) {
130
+ const tableName = process.env[`${ENV_TABLE_PREFIX}${key}`];
131
+ if (!tableName) {
132
+ throw new Error(`Missing environment variable ${ENV_TABLE_PREFIX}${key} for dep "${key}"`);
133
+ }
134
+ result[key] = createTableClient(tableName);
135
+ }
136
+ return result;
137
+ };
138
+ var buildParams = async (params) => {
139
+ if (!params) return void 0;
140
+ const entries = [];
141
+ for (const propName of Object.keys(params)) {
142
+ const ssmPath = process.env[`${ENV_PARAM_PREFIX}${propName}`];
143
+ if (!ssmPath) {
144
+ throw new Error(`Missing environment variable ${ENV_PARAM_PREFIX}${propName} for param "${propName}"`);
145
+ }
146
+ entries.push({ propName, ssmPath });
147
+ }
148
+ if (entries.length === 0) return void 0;
149
+ const values = await getParameters(entries.map((e) => e.ssmPath));
150
+ const result = {};
151
+ for (const { propName, ssmPath } of entries) {
152
+ const raw = values.get(ssmPath) ?? "";
153
+ const ref = params[propName];
154
+ result[propName] = ref?.transform ? ref.transform(raw) : raw;
155
+ }
156
+ return result;
157
+ };
158
+
159
+ export {
160
+ createTableClient,
161
+ buildDeps,
162
+ buildParams
163
+ };