effortless-aws 0.28.0 → 0.29.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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  AUTH_COOKIE_NAME,
3
3
  createHandlerRuntime
4
- } from "../chunk-EZG3NX42.js";
4
+ } from "../chunk-ISJC6CHC.js";
5
5
 
6
6
  // src/runtime/wrap-api.ts
7
7
  var CONTENT_TYPE_MAP = {
@@ -23,32 +23,6 @@ var parseBody = (body, isBase64) => {
23
23
  return decoded;
24
24
  }
25
25
  };
26
- var buildGetMatchers = (routes, basePath) => Object.entries(routes).map(([pattern, handler]) => {
27
- const fullPattern = (basePath + pattern).replace(/\/\/+/g, "/");
28
- const paramNames = [];
29
- const regexStr = fullPattern.replace(/\{(\w+)\}/g, (_, name) => {
30
- paramNames.push(name);
31
- return "([^/]+)";
32
- });
33
- return {
34
- regex: new RegExp(`^${regexStr}$`),
35
- paramNames,
36
- handler
37
- };
38
- });
39
- var matchRoute = (matchers, path) => {
40
- for (const matcher of matchers) {
41
- const match = path.match(matcher.regex);
42
- if (match) {
43
- const params = {};
44
- matcher.paramNames.forEach((name, i) => {
45
- params[name] = match[i + 1];
46
- });
47
- return { handler: matcher.handler, params };
48
- }
49
- }
50
- return null;
51
- };
52
26
  var toResult = (r) => {
53
27
  const resolved = r.contentType ? CONTENT_TYPE_MAP[r.contentType] : void 0;
54
28
  const customContentType = resolved ?? r.headers?.["content-type"] ?? r.headers?.["Content-Type"];
@@ -74,14 +48,14 @@ var unauthorized = () => ({
74
48
  headers: { "Content-Type": "application/json" },
75
49
  body: JSON.stringify({ error: "Unauthorized" })
76
50
  });
77
- var isPublicPath = (path, patterns) => patterns.some(
78
- (p) => p.endsWith("*") ? path.startsWith(p.slice(0, -1)) : path === p
51
+ var findRoute = (routes, method, relativePath) => routes.find(
52
+ (r) => r.path === relativePath && (r.method === method || r.method === "GET" && method === "HEAD")
79
53
  );
80
54
  var wrapApi = (handler) => {
81
55
  const rt = createHandlerRuntime(handler, "api", handler.__spec.lambda?.logLevel ?? "info");
82
56
  const basePath = handler.__spec.basePath;
83
57
  const isStream = handler.__spec.stream === true;
84
- const getMatchers = handler.get ? buildGetMatchers(handler.get, basePath) : [];
58
+ const routes = handler.routes ?? [];
85
59
  const defaultError = (error, status) => {
86
60
  console.error(`[effortless:${rt.handlerName}]`, error);
87
61
  return toResult({
@@ -92,91 +66,89 @@ var wrapApi = (handler) => {
92
66
  }
93
67
  });
94
68
  };
69
+ const extractRelativePath = (fullPath) => {
70
+ const prefix = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
71
+ if (!fullPath.startsWith(prefix)) return null;
72
+ const rest = fullPath.slice(prefix.length);
73
+ if (rest === "" || rest === "/") return "/";
74
+ if (!rest.startsWith("/")) return null;
75
+ return rest;
76
+ };
95
77
  const handleRequest = async (event, streamCtx) => {
96
78
  const startTime = Date.now();
97
79
  rt.patchConsole();
98
80
  let sharedArgs;
81
+ let ctxProps = {};
99
82
  try {
83
+ const method = event.requestContext?.http?.method ?? event.httpMethod ?? "GET";
84
+ const path = event.requestContext?.http?.path ?? event.path ?? "/";
85
+ const headers = event.headers ?? {};
86
+ const query = event.queryStringParameters ?? {};
87
+ const params = event.pathParameters ?? {};
88
+ const body = parseBody(event.body, event.isBase64Encoded ?? false);
89
+ const merged = {
90
+ ...query,
91
+ ...typeof body === "object" && body !== null ? body : {},
92
+ ...params
93
+ };
100
94
  const req = {
101
- method: event.requestContext?.http?.method ?? event.httpMethod ?? "GET",
102
- path: event.requestContext?.http?.path ?? event.path ?? "/",
103
- headers: event.headers ?? {},
104
- query: event.queryStringParameters ?? {},
105
- params: event.pathParameters ?? {},
106
- body: parseBody(event.body, event.isBase64Encoded ?? false),
95
+ method,
96
+ path,
97
+ headers,
98
+ query,
99
+ params,
100
+ body,
107
101
  rawBody: event.body
108
102
  };
109
- const input = { method: req.method, path: req.path, query: req.query, body: req.body };
103
+ const logInput = { method, path, query, body };
104
+ const relativePath = extractRelativePath(req.path);
105
+ if (!relativePath) {
106
+ rt.logExecution(startTime, logInput, { status: 404 });
107
+ return notFound();
108
+ }
109
+ const entry = findRoute(routes, req.method, relativePath);
110
+ if (!entry) {
111
+ rt.logExecution(startTime, logInput, { status: 404 });
112
+ return notFound();
113
+ }
110
114
  const cookieHeader = req.headers["cookie"] ?? req.headers["Cookie"] ?? "";
111
115
  let authCookie;
112
116
  if (cookieHeader) {
113
117
  const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${AUTH_COOKIE_NAME}=([^;]+)`));
114
118
  if (match) authCookie = match[1];
115
119
  }
116
- const authHeaderName = handler.apiToken?.header ?? "authorization";
120
+ const authHeaderName = "authorization";
117
121
  const authHeader = req.headers[authHeaderName] ?? req.headers[authHeaderName.toLowerCase()] ?? void 0;
118
- sharedArgs = await rt.commonArgs(authCookie, authHeader);
119
- if (handler.auth && sharedArgs.auth) {
120
- const auth = sharedArgs.auth;
121
- const publicPaths = handler.auth.public ?? [];
122
- const routePath = req.path.replace(new RegExp(`^${basePath}`), "") || "/";
123
- if (!auth.session && !isPublicPath(routePath, publicPaths) && !isPublicPath(req.path, publicPaths)) {
124
- rt.logExecution(startTime, input, { status: 401 });
122
+ sharedArgs = await rt.commonArgs(authCookie, authHeader, req.headers);
123
+ if (sharedArgs.auth) {
124
+ const auth2 = sharedArgs.auth;
125
+ if (!auth2.session && !entry.public) {
126
+ rt.logExecution(startTime, logInput, { status: 401 });
125
127
  return unauthorized();
126
128
  }
127
129
  }
128
- if (req.method === "GET" || req.method === "HEAD") {
129
- const matched = matchRoute(getMatchers, req.path);
130
- if (!matched) {
131
- rt.logExecution(startTime, input, { status: 404 });
132
- return notFound();
133
- }
134
- req.params = { ...req.params, ...matched.params };
135
- const args = { req, ...sharedArgs };
136
- if (streamCtx) args.stream = streamCtx.stream;
137
- try {
138
- const response = await matched.handler(args);
139
- if (response) {
140
- rt.logExecution(startTime, input, response.body);
141
- return toResult(response);
142
- }
143
- rt.logExecution(startTime, input, "[stream]");
144
- return void 0;
145
- } catch (error) {
146
- rt.logError(startTime, input, error);
147
- return handler.onError ? toResult(handler.onError({ error, req, ...sharedArgs })) : defaultError(error, 500);
148
- }
149
- }
150
- if (req.method === "POST" && handler.post) {
151
- const args = { req, ...sharedArgs };
152
- if (streamCtx) args.stream = streamCtx.stream;
153
- if (handler.schema) {
154
- try {
155
- args.data = handler.schema(req.body);
156
- } catch (error) {
157
- rt.logError(startTime, input, error);
158
- return handler.onError ? toResult(handler.onError({ error, req, ...sharedArgs })) : defaultError(error, 400);
159
- }
160
- }
161
- try {
162
- const response = await handler.post(args);
163
- if (response) {
164
- rt.logExecution(startTime, input, response.body);
165
- return toResult(response);
166
- }
167
- rt.logExecution(startTime, input, "[stream]");
168
- return void 0;
169
- } catch (error) {
170
- rt.logError(startTime, input, error);
171
- return handler.onError ? toResult(handler.onError({ error, req, ...sharedArgs })) : defaultError(error, 500);
130
+ const { ctx, auth, ...rest } = sharedArgs;
131
+ ctxProps = ctx && typeof ctx === "object" ? { ...ctx } : {};
132
+ delete ctxProps.auth;
133
+ const args = { ...ctxProps, req, input: merged, ...rest };
134
+ if (auth) args.auth = auth;
135
+ if (streamCtx) args.stream = streamCtx.stream;
136
+ try {
137
+ const response = await entry.onRequest(args);
138
+ if (response) {
139
+ rt.logExecution(startTime, logInput, response.body);
140
+ return toResult(response);
172
141
  }
142
+ rt.logExecution(startTime, logInput, "[stream]");
143
+ return void 0;
144
+ } catch (error) {
145
+ rt.logError(startTime, logInput, error);
146
+ return handler.onError ? toResult(handler.onError({ error, req, ...ctxProps })) : defaultError(error, 500);
173
147
  }
174
- rt.logExecution(startTime, input, { status: 404 });
175
- return notFound();
176
148
  } finally {
177
149
  if (handler.onAfterInvoke && sharedArgs) {
178
150
  try {
179
- await handler.onAfterInvoke(sharedArgs);
151
+ await handler.onAfterInvoke(ctxProps);
180
152
  } catch (e) {
181
153
  console.error(`[effortless:${rt.handlerName}] onAfterInvoke error`, e);
182
154
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createBucketClient,
3
3
  createHandlerRuntime
4
- } from "../chunk-EZG3NX42.js";
4
+ } from "../chunk-ISJC6CHC.js";
5
5
 
6
6
  // src/runtime/wrap-bucket.ts
7
7
  var ENV_DEP_SELF = "EFF_DEP_SELF";
@@ -26,11 +26,15 @@ var wrapBucket = (handler) => {
26
26
  return async (event) => {
27
27
  const startTime = Date.now();
28
28
  rt.patchConsole();
29
- let shared;
29
+ let ctxProps = {};
30
30
  try {
31
31
  const rawRecords = event.Records ?? [];
32
32
  const input = { recordCount: rawRecords.length };
33
- shared = { ...await rt.commonArgs(), bucket: getSelfClient() };
33
+ const common = await rt.commonArgs();
34
+ const ctx = common.ctx;
35
+ ctxProps = ctx && typeof ctx === "object" ? { ...ctx } : {};
36
+ const bucket = getSelfClient();
37
+ const shared = { bucket, ...ctxProps };
34
38
  let errorCount = 0;
35
39
  for (const record of rawRecords) {
36
40
  const bucketEvent = {
@@ -58,9 +62,9 @@ var wrapBucket = (handler) => {
58
62
  rt.logExecution(startTime, input, { processedCount: rawRecords.length });
59
63
  }
60
64
  } finally {
61
- if (handler.onAfterInvoke && shared) {
65
+ if (handler.onAfterInvoke) {
62
66
  try {
63
- await handler.onAfterInvoke(shared);
67
+ await handler.onAfterInvoke(ctxProps);
64
68
  } catch (e) {
65
69
  console.error(`[effortless:${rt.handlerName}] onAfterInvoke error`, e);
66
70
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createHandlerRuntime
3
- } from "../chunk-EZG3NX42.js";
3
+ } from "../chunk-ISJC6CHC.js";
4
4
 
5
5
  // src/runtime/wrap-fifo-queue.ts
6
6
  var parseMessages = (rawRecords, schema) => {
@@ -29,19 +29,22 @@ var parseMessages = (rawRecords, schema) => {
29
29
  return messages;
30
30
  };
31
31
  var wrapFifoQueue = (handler) => {
32
- if (!handler.onMessage && !handler.onBatch) {
33
- throw new Error("wrapFifoQueue requires a handler with onMessage or onBatch defined");
32
+ if (!handler.onMessage && !handler.onMessageBatch) {
33
+ throw new Error("wrapFifoQueue requires a handler with onMessage or onMessageBatch defined");
34
34
  }
35
35
  const rt = createHandlerRuntime(handler, "fifo-queue", handler.__spec.lambda?.logLevel ?? "info");
36
36
  const handleError = handler.onError ?? (({ error }) => console.error(`[effortless:${rt.handlerName}]`, error));
37
37
  return async (event) => {
38
38
  const startTime = Date.now();
39
39
  rt.patchConsole();
40
- let shared;
40
+ let ctxProps = {};
41
41
  try {
42
42
  const rawRecords = event.Records ?? [];
43
43
  const input = { messageCount: rawRecords.length };
44
- shared = await rt.commonArgs();
44
+ const common = await rt.commonArgs();
45
+ const ctx = common.ctx;
46
+ ctxProps = ctx && typeof ctx === "object" ? { ...ctx } : {};
47
+ const shared = { ...ctxProps };
45
48
  let messages;
46
49
  try {
47
50
  messages = parseMessages(rawRecords, handler.schema);
@@ -53,9 +56,14 @@ var wrapFifoQueue = (handler) => {
53
56
  };
54
57
  }
55
58
  const batchItemFailures = [];
56
- if (handler.onBatch) {
59
+ if (handler.onMessageBatch) {
57
60
  try {
58
- await handler.onBatch({ messages, ...shared });
61
+ const result = await handler.onMessageBatch({ messages, ...shared });
62
+ if (result?.failures) {
63
+ for (const id of result.failures) {
64
+ batchItemFailures.push({ itemIdentifier: id });
65
+ }
66
+ }
59
67
  } catch (error) {
60
68
  handleError({ error, ...shared });
61
69
  for (const message of messages) {
@@ -80,9 +88,9 @@ var wrapFifoQueue = (handler) => {
80
88
  }
81
89
  return { batchItemFailures };
82
90
  } finally {
83
- if (handler.onAfterInvoke && shared) {
91
+ if (handler.onAfterInvoke) {
84
92
  try {
85
- await handler.onAfterInvoke(shared);
93
+ await handler.onAfterInvoke(ctxProps);
86
94
  } catch (e) {
87
95
  console.error(`[effortless:${rt.handlerName}] onAfterInvoke error`, e);
88
96
  }
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createHandlerRuntime,
3
3
  createTableClient
4
- } from "../chunk-EZG3NX42.js";
4
+ } from "../chunk-ISJC6CHC.js";
5
5
 
6
6
  // src/runtime/wrap-table-stream.ts
7
7
  import { unmarshall } from "@aws-sdk/util-dynamodb";
@@ -36,20 +36,13 @@ var parseRecords = (rawRecords, schema) => {
36
36
  }
37
37
  return { records, sequenceNumbers };
38
38
  };
39
- var collectFailures = (records, sequenceNumbers) => {
40
- const failures = [];
41
- for (const record of records) {
42
- const seq = sequenceNumbers.get(record);
43
- if (seq) failures.push({ itemIdentifier: seq });
44
- }
45
- return failures;
46
- };
47
39
  var ENV_DEP_SELF = "EFF_DEP_SELF";
48
40
  var wrapTableStream = (handler) => {
49
- if (!handler.onRecord && !handler.onBatch) {
50
- throw new Error("wrapTableStream requires a handler with onRecord or onBatch defined");
41
+ if (!handler.onRecord && !handler.onRecordBatch) {
42
+ throw new Error("wrapTableStream requires a handler with onRecord or onRecordBatch defined");
51
43
  }
52
44
  const tagField = handler.__spec.tagField ?? "tag";
45
+ const concurrency = handler.__spec.concurrency ?? 1;
53
46
  let selfClient = null;
54
47
  const getSelfClient = () => {
55
48
  if (selfClient) return selfClient;
@@ -67,11 +60,14 @@ var wrapTableStream = (handler) => {
67
60
  return async (event) => {
68
61
  const startTime = Date.now();
69
62
  rt.patchConsole();
70
- let shared;
63
+ let ctxProps = {};
71
64
  try {
72
65
  const rawRecords = event.Records ?? [];
73
66
  const input = { recordCount: rawRecords.length };
74
- shared = { ...await rt.commonArgs(), table: getSelfClient() };
67
+ const common = await rt.commonArgs();
68
+ const ctx = common.ctx;
69
+ ctxProps = ctx && typeof ctx === "object" ? { ...ctx } : {};
70
+ const shared = { ...ctxProps };
75
71
  let records;
76
72
  let sequenceNumbers;
77
73
  try {
@@ -82,37 +78,47 @@ var wrapTableStream = (handler) => {
82
78
  return { batchItemFailures: rawRecords.map((r) => r.dynamodb?.SequenceNumber).filter((s) => !!s).map((seq) => ({ itemIdentifier: seq })) };
83
79
  }
84
80
  const batchItemFailures = [];
85
- if (handler.onBatch) {
81
+ const frozenBatch = Object.freeze(records);
82
+ if (handler.onRecordBatch) {
86
83
  try {
87
- await handler.onBatch({ records, ...shared });
84
+ const result = await handler.onRecordBatch({ records: frozenBatch, ...shared });
85
+ if (result?.failures) {
86
+ for (const seq of result.failures) {
87
+ batchItemFailures.push({ itemIdentifier: seq });
88
+ }
89
+ }
88
90
  } catch (error) {
89
91
  handleError({ error, ...shared });
90
- batchItemFailures.push(...collectFailures(records, sequenceNumbers));
91
- }
92
- } else {
93
- const results = [];
94
- const failures = [];
95
- const onRecord = handler.onRecord;
96
- for (const record of records) {
97
- try {
98
- const result = await onRecord({ record, ...shared });
99
- if (result !== void 0) results.push(result);
100
- } catch (error) {
101
- handleError({ error, ...shared });
102
- failures.push({ record, error });
92
+ for (const record of records) {
103
93
  const seq = sequenceNumbers.get(record);
104
94
  if (seq) batchItemFailures.push({ itemIdentifier: seq });
105
95
  }
106
96
  }
107
- if (handler.onBatchComplete) {
108
- try {
109
- await handler.onBatchComplete({ results, failures, ...shared });
110
- } catch (error) {
111
- handleError({ error, ...shared });
112
- for (const record of records) {
97
+ } else {
98
+ const onRecord = handler.onRecord;
99
+ if (concurrency <= 1) {
100
+ for (const record of records) {
101
+ try {
102
+ await onRecord({ record, batch: frozenBatch, ...shared });
103
+ } catch (error) {
104
+ handleError({ error, ...shared });
113
105
  const seq = sequenceNumbers.get(record);
114
- if (seq && !batchItemFailures.some((f) => f.itemIdentifier === seq)) {
115
- batchItemFailures.push({ itemIdentifier: seq });
106
+ if (seq) batchItemFailures.push({ itemIdentifier: seq });
107
+ }
108
+ }
109
+ } else {
110
+ for (let i = 0; i < records.length; i += concurrency) {
111
+ const chunk = records.slice(i, i + concurrency);
112
+ const results = await Promise.allSettled(
113
+ chunk.map((record) => onRecord({ record, batch: frozenBatch, ...shared }))
114
+ );
115
+ for (let j = 0; j < results.length; j++) {
116
+ const result = results[j];
117
+ const record = chunk[j];
118
+ if (result.status === "rejected") {
119
+ handleError({ error: result.reason, ...shared });
120
+ const seq = sequenceNumbers.get(record);
121
+ if (seq) batchItemFailures.push({ itemIdentifier: seq });
116
122
  }
117
123
  }
118
124
  }
@@ -125,9 +131,9 @@ var wrapTableStream = (handler) => {
125
131
  }
126
132
  return { batchItemFailures };
127
133
  } finally {
128
- if (handler.onAfterInvoke && shared) {
134
+ if (handler.onAfterInvoke) {
129
135
  try {
130
- await handler.onAfterInvoke(shared);
136
+ await handler.onAfterInvoke(ctxProps);
131
137
  } catch (e) {
132
138
  console.error(`[effortless:${rt.handlerName}] onAfterInvoke error`, e);
133
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effortless-aws",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Code-first AWS Lambda framework. Export handlers, deploy with one command.",
@@ -45,10 +45,13 @@
45
45
  "@types/node": "^22.19.9",
46
46
  "tsup": "^8.5.1",
47
47
  "type-fest": "^4.22.1",
48
- "typescript": "^5.9.3"
48
+ "typescript": "^5.9.3",
49
+ "vite-tsconfig-paths": "^6.1.1",
50
+ "vitest": "^4.0.18"
49
51
  },
50
52
  "scripts": {
51
53
  "build": "tsup",
52
- "typecheck": "tsc --noEmit"
54
+ "typecheck": "tsc --noEmit",
55
+ "test": "vitest run"
53
56
  }
54
57
  }