effortless-aws 0.4.1 → 0.4.2

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 CHANGED
@@ -1,14 +1,9 @@
1
1
  # effortless-aws
2
2
 
3
- AWS serverless is the best way to run production software. Lambda gives you 99.95% availability out of the box, scales to zero, handles thousands of concurrent requests, and you never manage a server. DynamoDB, SQS, S3, EventBridge — these are battle-tested building blocks with guarantees most self-hosted infrastructure can't match. The event-driven model maps naturally to real business logic: an order is placed, a file is uploaded, a record changes.
3
+ [![npm version](https://img.shields.io/npm/v/effortless-aws)](https://www.npmjs.com/package/effortless-aws)
4
+ [![npm downloads](https://img.shields.io/npm/dm/effortless-aws)](https://www.npmjs.com/package/effortless-aws)
4
5
 
5
- The problem is never AWS itself — it's the tooling. CloudFormation templates, IAM policies, Terraform state files, CDK constructs. You end up spending more time on infrastructure plumbing than on your actual product. And even when infrastructure is sorted out, wiring serverless resources together — connecting a Lambda to a DynamoDB stream, granting cross-service permissions, passing table names between functions — is tedious and error-prone.
6
-
7
- **Effortless** is a TypeScript framework for developers who build on AWS serverless. It handles three things:
8
-
9
- 1. **Infrastructure from code.** You write handlers, export them, and deploy. The framework derives Lambda functions, API Gateway routes, DynamoDB tables, streams, and IAM roles directly from your TypeScript exports. No config files.
10
- 2. **Bundling and packaging.** Your code is automatically bundled with [esbuild](https://esbuild.github.io/) — tree-shaken, split per function, with shared dependencies extracted into a common [Lambda Layer](https://docs.aws.amazon.com/lambda/latest/dg/chapter-layers.html). No build config to maintain.
11
- 3. **Typed cross-resource communication.** Reference one handler from another with `deps: { orders }` and get a fully typed client injected at runtime, with IAM permissions wired automatically. Serverless resources talk to each other through code, not through copied ARNs and manual policies.
6
+ TypeScript framework for AWS serverless. Export handlers, deploy with one command. No YAML, no CloudFormation, no state files.
12
7
 
13
8
  ```bash
14
9
  npm install effortless-aws
@@ -32,236 +27,22 @@ export const hello = defineHttp({
32
27
  npx eff deploy
33
28
  ```
34
29
 
35
- That's it — one export, one command. No YAML, no CloudFormation, no state files.
36
-
37
- ## Why
38
-
39
- 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.
40
-
41
- **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.
42
-
43
- ## Killer features
44
-
45
- **Infrastructure from code** — export a handler, get the AWS resources. No config files, no YAML.
46
-
47
- **Typed everything** — `defineTable` schema gives you typed `table.put()`, typed `deps.orders.get()`, typed `record.new`. One definition, types flow everywhere.
48
-
49
- **Direct AWS SDK deploys** — no CloudFormation, no Pulumi. Direct API calls. Deploy in ~5-10s, not 5-10 minutes.
50
-
51
- **No state files** — AWS resource tags are the source of truth. No tfstate, no S3 backends, no drift.
52
-
53
- **Cross-handler deps** — `deps: { orders }` auto-wires IAM permissions and injects a typed `TableClient`. Zero config.
54
-
55
- **SSM params** — `param("stripe-key")` fetches from Parameter Store at cold start. Auto IAM, auto caching, supports transforms.
56
-
57
- **Partial batch failures** — DynamoDB stream processing reports failed records individually. No batch-level retries for one bad record.
58
-
59
- **Cold start caching** — `context` factory runs once per cold start, cached across invocations. Put DB connections, SDK clients, config there.
60
-
61
- ## Examples
62
-
63
- ### Path params
64
-
65
- ```typescript
66
- export const getUser = defineHttp({
67
- method: "GET",
68
- path: "/users/{id}",
69
- onRequest: async ({ req }) => {
70
- const user = await findUser(req.params.id);
71
- return { status: 200, body: user };
72
- },
73
- });
74
- ```
75
-
76
- > Creates: Lambda function, API Gateway `GET /users/{id}` route, IAM execution role.
77
-
78
- ### Schema validation
79
-
80
- Works with any validation library — Zod, Effect Schema, or a plain function.
81
-
82
- ```typescript
83
- export const createUser = defineHttp({
84
- method: "POST",
85
- path: "/users",
86
- schema: (input) => parseUser(input),
87
- onRequest: async ({ data }) => {
88
- // data is typed from schema return type
89
- return { status: 201, body: { id: data.id } };
90
- },
91
- });
92
- ```
93
-
94
- ### Context (cold-start cache)
95
-
96
- `context` runs once per cold start. Put SDK clients, DB connections, config here.
97
-
98
- ```typescript
99
- export const listOrders = defineHttp({
100
- method: "GET",
101
- path: "/orders",
102
- context: () => ({
103
- db: new DatabaseClient(),
104
- }),
105
- onRequest: async ({ ctx }) => {
106
- const orders = await ctx.db.findAll();
107
- return { status: 200, body: orders };
108
- },
109
- });
110
- ```
111
-
112
- ### SSM params
113
-
114
- `param("key")` reads from Parameter Store at cold start. Auto IAM, auto caching.
115
-
116
- ```typescript
117
- export const charge = defineHttp({
118
- method: "POST",
119
- path: "/charge",
120
- params: { apiKey: param("stripe-key") },
121
- context: async ({ params }) => ({
122
- stripe: new Stripe(params.apiKey),
123
- }),
124
- onRequest: async ({ ctx, data }) => {
125
- await ctx.stripe.charges.create(data);
126
- return { status: 200, body: { ok: true } };
127
- },
128
- });
129
- ```
130
-
131
- > Creates: Lambda, API Gateway route, IAM role with `ssm:GetParameter` on `/{project}/{stage}/stripe-key`.
132
-
133
- ### Cross-handler deps
134
-
135
- `deps: { orders }` auto-wires IAM and injects a typed `TableClient`.
136
-
137
- ```typescript
138
- type Order = { id: string; name: string };
139
-
140
- export const orders = defineTable<Order>({
141
- pk: { name: "id", type: "string" },
142
- });
143
-
144
- export const createOrder = defineHttp({
145
- method: "POST",
146
- path: "/orders",
147
- deps: { orders },
148
- onRequest: async ({ deps }) => {
149
- await deps.orders.put({ id: crypto.randomUUID(), name: "New order" });
150
- return { status: 201, body: { ok: true } };
151
- },
152
- });
153
- ```
154
-
155
- > Creates: DynamoDB table, Lambda, API Gateway route. The Lambda's IAM role gets DynamoDB read/write permissions on the `orders` table automatically. Table name is injected via environment variable.
156
-
157
- ### DynamoDB table (resource only)
158
-
159
- Export a table — get the DynamoDB resource. No Lambda, no stream.
160
-
161
- ```typescript
162
- export const sessions = defineTable({
163
- pk: { name: "id", type: "string" },
164
- ttlAttribute: "expiresAt",
165
- });
166
- ```
167
-
168
- > Creates: DynamoDB table with TTL enabled. No Lambda, no stream — just the table.
169
-
170
- ### DynamoDB table with stream
171
-
172
- Add `onRecord` to process changes. Each record is handled individually with automatic partial batch failure reporting.
173
-
174
- ```typescript
175
- type User = { id: string; email: string; name: string };
176
-
177
- export const users = defineTable<User>({
178
- pk: { name: "id", type: "string" },
179
- sk: { name: "email", type: "string" },
180
- onRecord: async ({ record }) => {
181
- if (record.eventName === "INSERT") {
182
- await sendWelcomeEmail(record.new!.email);
183
- }
184
- },
185
- });
186
- ```
187
-
188
- > Creates: DynamoDB table with stream enabled, Lambda for stream processing, event source mapping between them, IAM role with DynamoDB read/write permissions. Failed records are reported individually via [partial batch responses](https://docs.aws.amazon.com/lambda/latest/dg/with-ddb.html#services-ddb-batchfailurereporting).
189
-
190
- ### Full example
191
-
192
- Everything together — table, HTTP handler with validation, deps, params, and context.
193
-
194
- ```typescript
195
- type Order = { id: string; total: number; chargeId?: string };
196
-
197
- export const orders = defineTable<Order>({
198
- pk: { name: "id", type: "string" },
199
- onRecord: async ({ record }) => {
200
- if (record.eventName === "INSERT") {
201
- await notifyWarehouse(record.new!);
202
- }
203
- },
204
- });
205
-
206
- export const createOrder = defineHttp({
207
- method: "POST",
208
- path: "/orders",
209
- schema: (input) => parseOrder(input),
210
- deps: { orders },
211
- params: { apiKey: param("stripe-key") },
212
- context: async ({ params }) => ({
213
- stripe: new Stripe(params.apiKey),
214
- }),
215
- onRequest: async ({ data, ctx, deps }) => {
216
- const charge = await ctx.stripe.charges.create({ amount: data.total });
217
- await deps.orders.put({ id: crypto.randomUUID(), ...data, chargeId: charge.id });
218
- return { status: 201, body: { ok: true } };
219
- },
220
- });
221
- ```
222
-
223
- ## Configuration
224
-
225
- ```typescript
226
- // effortless.config.ts
227
- import { defineConfig } from "effortless-aws";
228
-
229
- export default defineConfig({
230
- name: "my-app",
231
- region: "eu-central-1",
232
- handlers: ["src/**/*.ts"],
233
- });
234
- ```
235
-
236
- ## CLI
237
-
238
- ```bash
239
- npx eff deploy # deploy all handlers
240
- npx eff deploy --stage prod # deploy to specific stage
241
- npx eff deploy --only users # deploy single handler
242
- npx eff destroy # remove all resources
243
- npx eff logs users --follow # stream CloudWatch logs
244
- npx eff list # show deployed resources
245
- ```
246
-
247
- ## How it works
30
+ One export, one command. Lambda, API Gateway route, and IAM role created automatically.
248
31
 
249
- 1. **Static analysis** (ts-morph) — reads your exports, extracts handler config from AST
250
- 2. **Bundle** (esbuild) — wraps each handler with a runtime adapter
251
- 3. **Deploy** (AWS SDK) — creates/updates Lambda, API Gateway, DynamoDB, IAM directly
32
+ ## Features
252
33
 
253
- No CloudFormation stacks. No Terraform state. Tags on AWS resources are the only state.
34
+ - **Infrastructure from code** export a handler, get the AWS resources. No config files.
35
+ - **Typed everything** — `defineTable<Order>` gives you typed `put()`, typed `deps.orders.get()`, typed `record.new`.
36
+ - **Direct AWS SDK deploys** — no CloudFormation. Deploy in ~5-10s, not minutes.
37
+ - **No state files** — AWS resource tags are the source of truth.
38
+ - **Cross-handler deps** — `deps: { orders }` auto-wires IAM and injects a typed `TableClient`.
39
+ - **SSM params** — `param("stripe-key")` fetches from Parameter Store at cold start. Auto IAM, auto caching.
40
+ - **Partial batch failures** — DynamoDB stream processing reports failed records individually.
41
+ - **Cold start caching** — `context` factory runs once per cold start, cached across invocations.
254
42
 
255
- ## Compared to
43
+ ## Documentation
256
44
 
257
- | | SST v3 | Nitric | Serverless | **Effortless** |
258
- |---|---|---|---|---|
259
- | Infra from code (not config) | No | Yes | No | **Yes** |
260
- | Typed client from schema | No | No | No | **Yes** |
261
- | No state files | No | No | No | **Yes** |
262
- | Deploy speed | ~30s | ~30s | minutes | **~5-10s** |
263
- | Runs in your AWS account | Yes | Yes | Yes | **Yes** |
264
- | Open source | Yes | Yes | Yes | **Yes** |
45
+ Full docs, examples, and API reference: **[effortless-aws docs](https://effortless-aws.website)**
265
46
 
266
47
  ## License
267
48
 
package/dist/cli/index.js CHANGED
@@ -71251,9 +71251,10 @@ var deployHttpHandlers = (ctx) => Effect_exports.gen(function* () {
71251
71251
  resolveDeps(fn2.depsKeys, ctx.tableNameMap),
71252
71252
  resolveParams(fn2.paramEntries, ctx.input.project, stage)
71253
71253
  );
71254
+ const observe = fn2.config.observe !== false;
71254
71255
  const withPlatform = {
71255
- depsEnv: { ...resolved?.depsEnv, ...ctx.platformEnv },
71256
- depsPermissions: [...resolved?.depsPermissions ?? [], ...ctx.platformPermissions]
71256
+ depsEnv: { ...resolved?.depsEnv, ...observe ? ctx.platformEnv : {} },
71257
+ depsPermissions: [...resolved?.depsPermissions ?? [], ...observe ? ctx.platformPermissions : []]
71257
71258
  };
71258
71259
  const { exportName, functionArn, config: config2 } = yield* deployLambda({
71259
71260
  input: deployInput,
@@ -71308,9 +71309,10 @@ var deployTableHandlers = (ctx) => Effect_exports.gen(function* () {
71308
71309
  resolveDeps(fn2.depsKeys, ctx.tableNameMap),
71309
71310
  resolveParams(fn2.paramEntries, ctx.input.project, stage)
71310
71311
  );
71312
+ const observe = fn2.config.observe !== false;
71311
71313
  const withPlatform = {
71312
- depsEnv: { ...resolved?.depsEnv, ...ctx.platformEnv },
71313
- depsPermissions: [...resolved?.depsPermissions ?? [], ...ctx.platformPermissions]
71314
+ depsEnv: { ...resolved?.depsEnv, ...observe ? ctx.platformEnv : {} },
71315
+ depsPermissions: [...resolved?.depsPermissions ?? [], ...observe ? ctx.platformPermissions : []]
71314
71316
  };
71315
71317
  const result = yield* deployTableFunction({
71316
71318
  input: deployInput,
@@ -71334,6 +71336,10 @@ var deployTableHandlers = (ctx) => Effect_exports.gen(function* () {
71334
71336
  }
71335
71337
  return results;
71336
71338
  });
71339
+ function buildSiteRoutePaths(configPath) {
71340
+ const basePath = configPath.replace(/\/+$/, "");
71341
+ return [basePath || "/", `${basePath}/{file+}`];
71342
+ }
71337
71343
  var deploySiteHandlers = (ctx) => Effect_exports.gen(function* () {
71338
71344
  const results = [];
71339
71345
  for (const { file: file6, exports } of ctx.handlers) {
@@ -71346,9 +71352,10 @@ var deploySiteHandlers = (ctx) => Effect_exports.gen(function* () {
71346
71352
  };
71347
71353
  if (ctx.input.stage) deployInput.stage = ctx.input.stage;
71348
71354
  for (const fn2 of exports) {
71355
+ const observe = fn2.config.observe === true;
71349
71356
  const withPlatform = {
71350
- depsEnv: { ...ctx.platformEnv },
71351
- depsPermissions: [...ctx.platformPermissions]
71357
+ depsEnv: observe ? { ...ctx.platformEnv } : {},
71358
+ depsPermissions: observe ? [...ctx.platformPermissions] : []
71352
71359
  };
71353
71360
  const { exportName, functionArn, config: config2, handlerName } = yield* deploySiteLambda({
71354
71361
  input: deployInput,
@@ -71365,13 +71372,13 @@ var deploySiteHandlers = (ctx) => Effect_exports.gen(function* () {
71365
71372
  })
71366
71373
  )
71367
71374
  );
71368
- const basePath = config2.path.replace(/\/+$/, "") || "/";
71375
+ const [rootPath, greedyPath] = buildSiteRoutePaths(config2.path);
71369
71376
  const { apiUrl: rootUrl } = yield* addRouteToApi({
71370
71377
  apiId: ctx.apiId,
71371
71378
  region: ctx.input.region,
71372
71379
  functionArn,
71373
71380
  method: "GET",
71374
- path: basePath
71381
+ path: rootPath
71375
71382
  }).pipe(
71376
71383
  Effect_exports.provide(
71377
71384
  clients_exports.makeClients({
@@ -71385,7 +71392,7 @@ var deploySiteHandlers = (ctx) => Effect_exports.gen(function* () {
71385
71392
  region: ctx.input.region,
71386
71393
  functionArn,
71387
71394
  method: "GET",
71388
- path: `${basePath}/{file+}`
71395
+ path: greedyPath
71389
71396
  }).pipe(
71390
71397
  Effect_exports.provide(
71391
71398
  clients_exports.makeClients({
@@ -71395,7 +71402,7 @@ var deploySiteHandlers = (ctx) => Effect_exports.gen(function* () {
71395
71402
  )
71396
71403
  );
71397
71404
  results.push({ exportName, url: rootUrl, functionArn });
71398
- yield* Effect_exports.logInfo(` GET ${basePath} \u2192 ${handlerName} (site)`);
71405
+ yield* Effect_exports.logInfo(` GET ${rootPath} \u2192 ${handlerName} (site)`);
71399
71406
  }
71400
71407
  }
71401
71408
  return results;