effortless-aws 0.2.1 → 0.3.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 CHANGED
@@ -1,6 +1,14 @@
1
1
  # effortless-aws
2
2
 
3
- Code-first AWS Lambda framework. Export handlers, deploy to AWS. No infrastructure files needed.
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.
4
+
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.
4
12
 
5
13
  ```bash
6
14
  npm install effortless-aws
@@ -9,32 +17,13 @@ npm install effortless-aws
9
17
  ## What it looks like
10
18
 
11
19
  ```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
- });
20
+ import { defineHttp } from "effortless-aws";
24
21
 
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 } };
22
+ export const hello = defineHttp({
23
+ method: "GET",
24
+ path: "/hello",
25
+ onRequest: async () => {
26
+ return { status: 200, body: { message: "Hello!" } };
38
27
  },
39
28
  });
40
29
  ```
@@ -43,7 +32,7 @@ export const createOrder = defineHttp({
43
32
  npx eff deploy
44
33
  ```
45
34
 
46
- That's it. No YAML, no CloudFormation, no state files.
35
+ That's it — one export, one command. No YAML, no CloudFormation, no state files.
47
36
 
48
37
  ## Why
49
38
 
@@ -69,34 +58,105 @@ Traditional Lambda development splits infrastructure and code across multiple fi
69
58
 
70
59
  **Cold start caching** — `context` factory runs once per cold start, cached across invocations. Put DB connections, SDK clients, config there.
71
60
 
72
- ## Handler types
61
+ ## Examples
73
62
 
74
- ### HTTP
63
+ ### Path params
75
64
 
76
65
  ```typescript
77
66
  export const getUser = defineHttp({
78
67
  method: "GET",
79
68
  path: "/users/{id}",
80
69
  onRequest: async ({ req }) => {
81
- return { status: 200, body: { id: req.params.id } };
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 };
82
108
  },
83
109
  });
84
110
  ```
85
111
 
86
- ### DynamoDB Table + Stream
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`.
87
136
 
88
137
  ```typescript
89
- export const users = defineTable({
138
+ type Order = { id: string; name: string };
139
+
140
+ export const orders = defineTable<Order>({
90
141
  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);
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 } };
95
151
  },
96
152
  });
97
153
  ```
98
154
 
99
- ### Table (resource only, no stream)
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.
100
160
 
101
161
  ```typescript
102
162
  export const sessions = defineTable({
@@ -105,6 +165,61 @@ export const sessions = defineTable({
105
165
  });
106
166
  ```
107
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
+
108
223
  ## Configuration
109
224
 
110
225
  ```typescript
@@ -113,6 +113,8 @@ var computeTtl = (ttlSeconds = DEFAULT_TTL_SECONDS) => Math.floor(Date.now() / 1
113
113
 
114
114
  // src/runtime/handler-utils.ts
115
115
  import { randomUUID } from "crypto";
116
+ import { readFileSync } from "fs";
117
+ import { join } from "path";
116
118
 
117
119
  // src/runtime/ssm-client.ts
118
120
  import { SSM } from "@aws-sdk/client-ssm";
@@ -253,6 +255,7 @@ var buildParams = async (params) => {
253
255
  }
254
256
  return result;
255
257
  };
258
+ var readStatic = (filePath) => readFileSync(join(process.cwd(), filePath), "utf-8");
256
259
  var createHandlerRuntime = (handler, handlerType) => {
257
260
  const platform = createPlatformClient();
258
261
  const handlerName = process.env.EFF_HANDLER ?? "unknown";
@@ -280,6 +283,7 @@ var createHandlerRuntime = (handler, handlerType) => {
280
283
  if (deps) args.deps = deps;
281
284
  const params = await getParams();
282
285
  if (params) args.params = params;
286
+ if (handler.static) args.readStatic = readStatic;
283
287
  return args;
284
288
  };
285
289
  const logExecution = (startTime, input, output) => {
package/dist/cli/index.js CHANGED
@@ -70498,7 +70498,7 @@ var parseSource = (source) => {
70498
70498
  const project2 = new Project({ useInMemoryFileSystem: true });
70499
70499
  return project2.createSourceFile("input.ts", source);
70500
70500
  };
70501
- var RUNTIME_PROPS = ["onRequest", "onRecord", "onBatchComplete", "onBatch", "context", "schema", "onError", "deps", "params"];
70501
+ var RUNTIME_PROPS = ["onRequest", "onRecord", "onBatchComplete", "onBatch", "context", "schema", "onError", "deps", "params", "static"];
70502
70502
  var buildConfigWithoutRuntime = (obj) => {
70503
70503
  const props = obj.getProperties().filter((p3) => {
70504
70504
  if (p3.getKind() === SyntaxKind.PropertyAssignment) {
@@ -70572,6 +70572,19 @@ var extractParamEntries = (obj) => {
70572
70572
  }
70573
70573
  return entries2;
70574
70574
  };
70575
+ var extractStaticGlobs = (obj) => {
70576
+ const staticProp = obj.getProperties().find((p3) => {
70577
+ if (p3.getKind() === SyntaxKind.PropertyAssignment) {
70578
+ return p3.getName() === "static";
70579
+ }
70580
+ return false;
70581
+ });
70582
+ if (!staticProp || staticProp.getKind() !== SyntaxKind.PropertyAssignment) return [];
70583
+ const init = staticProp.getInitializer();
70584
+ if (!init || init.getKind() !== SyntaxKind.ArrayLiteralExpression) return [];
70585
+ const arrayLiteral = init;
70586
+ return arrayLiteral.getElements().filter((e) => e.getKind() === SyntaxKind.StringLiteral).map((e) => e.asKindOrThrow(SyntaxKind.StringLiteral).getLiteralValue());
70587
+ };
70575
70588
  var handlerRegistry = {
70576
70589
  http: {
70577
70590
  defineFn: "defineHttp",
@@ -70605,7 +70618,8 @@ var extractHandlerConfigs = (source, type2) => {
70605
70618
  const hasHandler = handlerProps.some((p3) => extractPropertyFromObject(objLiteral, p3) !== void 0);
70606
70619
  const depsKeys = extractDepsKeys(objLiteral);
70607
70620
  const paramEntries = extractParamEntries(objLiteral);
70608
- results.push({ exportName: "default", config: configObj, hasHandler, depsKeys, paramEntries });
70621
+ const staticGlobs = extractStaticGlobs(objLiteral);
70622
+ results.push({ exportName: "default", config: configObj, hasHandler, depsKeys, paramEntries, staticGlobs });
70609
70623
  }
70610
70624
  }
70611
70625
  }
@@ -70626,7 +70640,8 @@ var extractHandlerConfigs = (source, type2) => {
70626
70640
  const hasHandler = handlerProps.some((p3) => extractPropertyFromObject(objLiteral, p3) !== void 0);
70627
70641
  const depsKeys = extractDepsKeys(objLiteral);
70628
70642
  const paramEntries = extractParamEntries(objLiteral);
70629
- results.push({ exportName: decl.getName(), config: configObj, hasHandler, depsKeys, paramEntries });
70643
+ const staticGlobs = extractStaticGlobs(objLiteral);
70644
+ results.push({ exportName: decl.getName(), config: configObj, hasHandler, depsKeys, paramEntries, staticGlobs });
70630
70645
  }
70631
70646
  });
70632
70647
  });
@@ -70687,8 +70702,27 @@ var zip12 = (input) => Effect_exports.async((resume2) => {
70687
70702
  archive.on("end", () => resume2(Effect_exports.succeed(Buffer.concat(chunks2))));
70688
70703
  archive.on("error", (err) => resume2(Effect_exports.fail(err)));
70689
70704
  archive.append(input.content, { name: input.filename ?? "index.mjs", date: FIXED_DATE2 });
70705
+ if (input.staticFiles) {
70706
+ for (const file6 of input.staticFiles) {
70707
+ archive.append(file6.content, { name: file6.zipPath, date: FIXED_DATE2 });
70708
+ }
70709
+ }
70690
70710
  archive.finalize();
70691
70711
  });
70712
+ var resolveStaticFiles = (globs, projectDir) => {
70713
+ const files = [];
70714
+ for (const pattern2 of globs) {
70715
+ const matches = globSync(pattern2, { cwd: projectDir });
70716
+ for (const match18 of matches) {
70717
+ const absPath = path5.join(projectDir, match18);
70718
+ files.push({
70719
+ content: fsSync2.readFileSync(absPath),
70720
+ zipPath: match18
70721
+ });
70722
+ }
70723
+ }
70724
+ return files;
70725
+ };
70692
70726
  var findHandlerFiles = (patterns, cwd) => {
70693
70727
  const files = /* @__PURE__ */ new Set();
70694
70728
  for (const pattern2 of patterns) {
@@ -70750,7 +70784,8 @@ var deployCoreLambda = ({
70750
70784
  layerArn,
70751
70785
  external,
70752
70786
  depsEnv,
70753
- depsPermissions
70787
+ depsPermissions,
70788
+ staticGlobs
70754
70789
  }) => Effect_exports.gen(function* () {
70755
70790
  const tagCtx = {
70756
70791
  project: input.project,
@@ -70779,7 +70814,8 @@ var deployCoreLambda = ({
70779
70814
  ...bundleType ? { type: bundleType } : {},
70780
70815
  ...external && external.length > 0 ? { external } : {}
70781
70816
  });
70782
- const code2 = yield* zip12({ content: bundled });
70817
+ const staticFiles = staticGlobs && staticGlobs.length > 0 ? resolveStaticFiles(staticGlobs, input.projectDir) : void 0;
70818
+ const code2 = yield* zip12({ content: bundled, staticFiles });
70783
70819
  const environment2 = {
70784
70820
  EFF_PROJECT: input.project,
70785
70821
  EFF_STAGE: tagCtx.stage,
@@ -70803,7 +70839,7 @@ var deployCoreLambda = ({
70803
70839
  });
70804
70840
 
70805
70841
  // src/deploy/deploy-http.ts
70806
- var deployLambda = ({ input, fn: fn2, layerArn, external, depsEnv, depsPermissions }) => Effect_exports.gen(function* () {
70842
+ var deployLambda = ({ input, fn: fn2, layerArn, external, depsEnv, depsPermissions, staticGlobs }) => Effect_exports.gen(function* () {
70807
70843
  const { exportName, config: config2 } = fn2;
70808
70844
  const handlerName = config2.name ?? exportName;
70809
70845
  const { functionArn } = yield* deployCoreLambda({
@@ -70817,7 +70853,8 @@ var deployLambda = ({ input, fn: fn2, layerArn, external, depsEnv, depsPermissio
70817
70853
  ...layerArn ? { layerArn } : {},
70818
70854
  ...external ? { external } : {},
70819
70855
  ...depsEnv ? { depsEnv } : {},
70820
- ...depsPermissions ? { depsPermissions } : {}
70856
+ ...depsPermissions ? { depsPermissions } : {},
70857
+ ...staticGlobs && staticGlobs.length > 0 ? { staticGlobs } : {}
70821
70858
  });
70822
70859
  return { exportName, functionArn, config: config2, handlerName };
70823
70860
  });
@@ -70940,7 +70977,7 @@ var deployAll = (input) => Effect_exports.gen(function* () {
70940
70977
 
70941
70978
  // src/deploy/deploy-table.ts
70942
70979
  var TABLE_DEFAULT_PERMISSIONS = ["dynamodb:*", "logs:*"];
70943
- var deployTableFunction = ({ input, fn: fn2, layerArn, external, depsEnv, depsPermissions }) => Effect_exports.gen(function* () {
70980
+ var deployTableFunction = ({ input, fn: fn2, layerArn, external, depsEnv, depsPermissions, staticGlobs }) => Effect_exports.gen(function* () {
70944
70981
  const { exportName, config: config2 } = fn2;
70945
70982
  const handlerName = config2.name ?? exportName;
70946
70983
  const tagCtx = {
@@ -70971,7 +71008,8 @@ var deployTableFunction = ({ input, fn: fn2, layerArn, external, depsEnv, depsPe
70971
71008
  ...layerArn ? { layerArn } : {},
70972
71009
  ...external ? { external } : {},
70973
71010
  depsEnv: selfEnv,
70974
- ...depsPermissions ? { depsPermissions } : {}
71011
+ ...depsPermissions ? { depsPermissions } : {},
71012
+ ...staticGlobs && staticGlobs.length > 0 ? { staticGlobs } : {}
70975
71013
  });
70976
71014
  yield* Effect_exports.logInfo("Setting up event source mapping...");
70977
71015
  yield* ensureEventSourceMapping({
@@ -71184,7 +71222,8 @@ var deployHttpHandlers = (ctx) => Effect_exports.gen(function* () {
71184
71222
  ...ctx.layerArn ? { layerArn: ctx.layerArn } : {},
71185
71223
  ...ctx.external.length > 0 ? { external: ctx.external } : {},
71186
71224
  depsEnv: withPlatform.depsEnv,
71187
- depsPermissions: withPlatform.depsPermissions
71225
+ depsPermissions: withPlatform.depsPermissions,
71226
+ ...fn2.staticGlobs.length > 0 ? { staticGlobs: fn2.staticGlobs } : {}
71188
71227
  }).pipe(
71189
71228
  Effect_exports.provide(
71190
71229
  clients_exports.makeClients({
@@ -71240,7 +71279,8 @@ var deployTableHandlers = (ctx) => Effect_exports.gen(function* () {
71240
71279
  ...ctx.layerArn ? { layerArn: ctx.layerArn } : {},
71241
71280
  ...ctx.external.length > 0 ? { external: ctx.external } : {},
71242
71281
  depsEnv: withPlatform.depsEnv,
71243
- depsPermissions: withPlatform.depsPermissions
71282
+ depsPermissions: withPlatform.depsPermissions,
71283
+ ...fn2.staticGlobs.length > 0 ? { staticGlobs: fn2.staticGlobs } : {}
71244
71284
  }).pipe(
71245
71285
  Effect_exports.provide(
71246
71286
  clients_exports.makeClients({