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 +153 -0
- package/dist/chunk-I5TS7O5S.js +163 -0
- package/dist/cli/index.js +453 -98
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +359 -214
- package/dist/index.js +21 -4
- package/dist/index.js.map +1 -1
- package/dist/runtime/wrap-http.js +86 -0
- package/dist/runtime/wrap-table-stream.js +143 -0
- package/package.json +3 -8
package/dist/index.js
CHANGED
|
@@ -3,29 +3,46 @@ var defineConfig = (config) => config;
|
|
|
3
3
|
|
|
4
4
|
// src/handlers/define-http.ts
|
|
5
5
|
var defineHttp = (options) => {
|
|
6
|
-
const { onRequest, context, ...config } = options;
|
|
6
|
+
const { onRequest, onError, context, schema, deps, params, ...config } = options;
|
|
7
7
|
return {
|
|
8
8
|
__brand: "effortless-http",
|
|
9
9
|
config,
|
|
10
|
+
...schema ? { schema } : {},
|
|
11
|
+
...onError ? { onError } : {},
|
|
10
12
|
...context ? { context } : {},
|
|
13
|
+
...deps ? { deps } : {},
|
|
14
|
+
...params ? { params } : {},
|
|
11
15
|
onRequest
|
|
12
16
|
};
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
// src/handlers/define-table.ts
|
|
16
20
|
var defineTable = (options) => {
|
|
17
|
-
const { onRecord, onBatchComplete, context, ...config } = options;
|
|
21
|
+
const { onRecord, onBatchComplete, onBatch, onError, schema, context, deps, params, ...config } = options;
|
|
18
22
|
return {
|
|
19
23
|
__brand: "effortless-table",
|
|
20
24
|
config,
|
|
25
|
+
...schema ? { schema } : {},
|
|
26
|
+
...onError ? { onError } : {},
|
|
21
27
|
...context ? { context } : {},
|
|
28
|
+
...deps ? { deps } : {},
|
|
29
|
+
...params ? { params } : {},
|
|
22
30
|
...onRecord ? { onRecord } : {},
|
|
23
|
-
...onBatchComplete ? { onBatchComplete } : {}
|
|
31
|
+
...onBatchComplete ? { onBatchComplete } : {},
|
|
32
|
+
...onBatch ? { onBatch } : {}
|
|
24
33
|
};
|
|
25
34
|
};
|
|
35
|
+
|
|
36
|
+
// src/handlers/param.ts
|
|
37
|
+
var param = (key, transform) => ({
|
|
38
|
+
__brand: "effortless-param",
|
|
39
|
+
key,
|
|
40
|
+
...transform ? { transform } : {}
|
|
41
|
+
});
|
|
26
42
|
export {
|
|
27
43
|
defineConfig,
|
|
28
44
|
defineHttp,
|
|
29
|
-
defineTable
|
|
45
|
+
defineTable,
|
|
46
|
+
param
|
|
30
47
|
};
|
|
31
48
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config.ts","../src/handlers/define-http.ts","../src/handlers/define-table.ts"],"sourcesContent":["/**\n * Configuration for an Effortless project.\n *\n * @example\n * ```typescript\n * // effortless.config.ts\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport type EffortlessConfig = {\n /**\n * Project name used for resource naming and tagging.\n * This becomes part of Lambda function names, IAM roles, etc.\n */\n name: string;\n\n /**\n * Default AWS region for all handlers.\n * Can be overridden per-handler or via CLI `--region` flag.\n * @default \"eu-central-1\"\n */\n region?: string;\n\n /**\n * Deployment stage (e.g., \"dev\", \"staging\", \"prod\").\n * Used for resource isolation and tagging.\n * @default \"dev\"\n */\n stage?: string;\n\n /**\n * Glob patterns or directory paths to scan for handlers.\n * Used by `eff deploy` (without file argument) to auto-discover handlers.\n *\n * @example\n * ```typescript\n * // Single directory - scans for all .ts files\n * handlers: \"src\"\n *\n * // Glob patterns\n * handlers: [\"src/**\\/*.ts\", \"lib/**\\/*.handler.ts\"]\n * ```\n */\n handlers?: string | string[];\n\n /**\n * Default settings applied to all handlers unless overridden.\n */\n defaults?: {\n /**\n * Lambda memory in MB.\n * @default 256\n */\n memory?: number;\n\n /**\n * Lambda timeout as a human-readable string.\n * @example \"30 seconds\", \"5 minutes\"\n */\n timeout?: string;\n\n /**\n * Lambda runtime.\n * @default \"nodejs22.x\"\n */\n runtime?: string;\n };\n};\n\n/**\n * Helper function for type-safe configuration.\n * Returns the config object as-is, but provides TypeScript autocompletion.\n *\n * @example\n * ```typescript\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport const defineConfig = (config: EffortlessConfig): EffortlessConfig => config;\n","import type { Permission } from \"./permissions\";\n\n/** HTTP methods supported by API Gateway */\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n\n/**\n * Incoming HTTP request object passed to the handler\n */\nexport type HttpRequest = {\n /** HTTP method (GET, POST, etc.) */\n method: string;\n /** Request path (e.g., \"/users/123\") */\n path: string;\n /** Request headers */\n headers: Record<string, string | undefined>;\n /** Query string parameters */\n query: Record<string, string | undefined>;\n /** Path parameters extracted from route (e.g., {id} -> params.id) */\n params: Record<string, string | undefined>;\n /** Parsed request body (JSON parsed if Content-Type is application/json) */\n body: unknown;\n /** Raw unparsed request body */\n rawBody?: string;\n};\n\n/**\n * HTTP response returned from the handler\n */\nexport type HttpResponse = {\n /** HTTP status code (e.g., 200, 404, 500) */\n status: number;\n /** Response body (will be JSON serialized) */\n body?: unknown;\n /** Response headers */\n headers?: Record<string, string>;\n};\n\n/**\n * Configuration options extracted from DefineHttpOptions (without onRequest callback)\n */\nexport type HttpConfig = {\n /** Handler name. Defaults to export name if not specified */\n name?: string;\n /** HTTP method for the route */\n method: HttpMethod;\n /** Route path (e.g., \"/api/users\", \"/api/users/{id}\") */\n path: string;\n /** Lambda memory in MB (default: 256) */\n memory?: number;\n /** Lambda timeout in seconds (default: 30) */\n timeout?: number;\n /** Additional IAM permissions for the Lambda */\n permissions?: Permission[];\n};\n\n/**\n * Handler function type for HTTP endpoints\n *\n * @typeParam C - Type of the context/dependencies (from context function)\n */\nexport type HttpHandlerFn<C = undefined> =\n C extends undefined\n ? (args: { req: HttpRequest }) => Promise<HttpResponse>\n : (args: { req: HttpRequest; ctx: C }) => Promise<HttpResponse>;\n\n/**\n * Options for defining an HTTP endpoint\n *\n * @typeParam C - Type of the context/dependencies returned by context function\n *\n * @example Without context\n * ```typescript\n * export const getUsers = defineHttp({\n * method: \"GET\",\n * path: \"/api/users\",\n * onRequest: async ({ req }) => ({\n * status: 200,\n * body: { users: [] }\n * })\n * });\n * ```\n *\n * @example With context (e.g., Effect runtime)\n * ```typescript\n * export const createOrder = defineHttp<typeof orderRuntime>({\n * method: \"POST\",\n * path: \"/api/orders\",\n * context: () => ManagedRuntime.make(\n * Layer.mergeAll(ConfigLive, DbClientLive)\n * ),\n * onRequest: async ({ req, ctx }) => {\n * const result = await ctx.runPromise(createOrderEffect(req.body));\n * return { status: 201, body: result };\n * }\n * });\n * ```\n */\nexport type DefineHttpOptions<C = undefined> = HttpConfig & {\n /**\n * Factory function to create context/dependencies for the request handler.\n * Called once on cold start, result is cached and reused across invocations.\n */\n context?: () => C;\n /** HTTP request handler function */\n onRequest: HttpHandlerFn<C>;\n};\n\n/**\n * Internal handler object created by defineHttp\n * @internal\n */\nexport type HttpHandler<C = undefined> = {\n readonly __brand: \"effortless-http\";\n readonly config: HttpConfig;\n readonly context?: () => C;\n readonly onRequest: HttpHandlerFn<C>;\n};\n\n/**\n * Define an HTTP endpoint that creates an API Gateway route + Lambda function\n *\n * @typeParam C - Type of the context/dependencies (inferred from context function)\n * @param options - Configuration, optional context factory, and request handler\n * @returns Handler object used by the deployment system\n *\n * @example Basic GET endpoint\n * ```typescript\n * export const hello = defineHttp({\n * method: \"GET\",\n * path: \"/hello\",\n * onRequest: async ({ req }) => ({\n * status: 200,\n * body: { message: \"Hello World!\" }\n * })\n * });\n * ```\n *\n * @example POST endpoint with body parsing\n * ```typescript\n * export const createUser = defineHttp({\n * method: \"POST\",\n * path: \"/users\",\n * memory: 512,\n * timeout: 30,\n * onRequest: async ({ req }) => {\n * const { name, email } = req.body as { name: string; email: string };\n * // ... create user\n * return { status: 201, body: { id: \"123\", name, email } };\n * }\n * });\n * ```\n *\n * @example With Effect runtime context\n * ```typescript\n * export const processPayment = defineHttp<typeof paymentRuntime>({\n * method: \"POST\",\n * path: \"/payments\",\n * context: () => ManagedRuntime.make(\n * Layer.mergeAll(ConfigLive, StripeClientLive)\n * ),\n * onRequest: async ({ req, ctx }) => {\n * const result = await ctx.runPromise(\n * processPaymentEffect(req.body)\n * );\n * return { status: 200, body: result };\n * }\n * });\n * ```\n */\nexport const defineHttp = <C = undefined>(\n options: DefineHttpOptions<C>\n): HttpHandler<C> => {\n const { onRequest, context, ...config } = options;\n return {\n __brand: \"effortless-http\",\n config,\n ...(context ? { context } : {}),\n onRequest\n } as HttpHandler<C>;\n};\n","import type { Permission } from \"./permissions\";\n\n/** DynamoDB attribute types for keys */\nexport type KeyType = \"string\" | \"number\" | \"binary\";\n\n/**\n * DynamoDB table key definition\n */\nexport type TableKey = {\n /** Attribute name */\n name: string;\n /** Attribute type */\n type: KeyType;\n};\n\n/** DynamoDB Streams view type - determines what data is captured in stream records */\nexport type StreamView = \"NEW_AND_OLD_IMAGES\" | \"NEW_IMAGE\" | \"OLD_IMAGE\" | \"KEYS_ONLY\";\n\n/**\n * Configuration options extracted from DefineTableOptions (without onRecord/context)\n */\nexport type TableConfig = {\n /** Table/handler name. Defaults to export name if not specified */\n name?: string;\n /** Partition key definition */\n pk: TableKey;\n /** Sort key definition (optional) */\n sk?: TableKey;\n /** DynamoDB billing mode (default: \"PAY_PER_REQUEST\") */\n billingMode?: \"PAY_PER_REQUEST\" | \"PROVISIONED\";\n /** TTL attribute name for automatic item expiration */\n ttlAttribute?: string;\n /** Stream view type - what data to include in stream records (default: \"NEW_AND_OLD_IMAGES\") */\n streamView?: StreamView;\n /** Number of records to process in each Lambda invocation (1-10000, default: 100) */\n batchSize?: number;\n /** Maximum time in seconds to gather records before invoking (0-300, default: 2) */\n batchWindow?: number;\n /** Where to start reading the stream (default: \"LATEST\") */\n startingPosition?: \"LATEST\" | \"TRIM_HORIZON\";\n /** Lambda memory in MB (default: 256) */\n memory?: number;\n /** Lambda timeout in seconds (default: 30) */\n timeout?: number;\n /** Additional IAM permissions for the Lambda */\n permissions?: Permission[];\n};\n\n/**\n * DynamoDB stream record passed to onRecord callback\n *\n * @typeParam T - Type of the table items (new/old values)\n */\nexport type TableRecord<T = Record<string, unknown>> = {\n /** Type of modification: INSERT, MODIFY, or REMOVE */\n eventName: \"INSERT\" | \"MODIFY\" | \"REMOVE\";\n /** New item value (present for INSERT and MODIFY) */\n new?: T;\n /** Old item value (present for MODIFY and REMOVE) */\n old?: T;\n /** Primary key of the affected item */\n keys: Record<string, unknown>;\n /** Sequence number for ordering */\n sequenceNumber?: string;\n /** Approximate timestamp when the modification occurred */\n timestamp?: number;\n};\n\n/**\n * Information about a failed record during batch processing\n *\n * @typeParam T - Type of the table items\n */\nexport type FailedRecord<T = Record<string, unknown>> = {\n /** The record that failed to process */\n record: TableRecord<T>;\n /** The error that occurred */\n error: unknown;\n};\n\n/**\n * Callback function type for processing a single DynamoDB stream record\n *\n * @typeParam T - Type of the table items\n * @typeParam C - Type of the context/dependencies (from context function)\n * @typeParam R - Return type (can be void or a value to accumulate)\n */\nexport type TableRecordFn<T = Record<string, unknown>, C = undefined, R = void> =\n C extends undefined\n ? (args: { record: TableRecord<T> }) => Promise<R>\n : (args: { record: TableRecord<T>; ctx: C }) => Promise<R>;\n\n/**\n * Callback function type for processing accumulated batch results\n *\n * @typeParam T - Type of the table items\n * @typeParam C - Type of the context/dependencies\n * @typeParam R - Type of results accumulated from onRecord\n */\nexport type TableBatchCompleteFn<T = Record<string, unknown>, C = undefined, R = void> =\n C extends undefined\n ? (args: { results: R[]; failures: FailedRecord<T>[] }) => Promise<void>\n : (args: { results: R[]; failures: FailedRecord<T>[]; ctx: C }) => Promise<void>;\n\n/**\n * Options for defining a DynamoDB table with optional stream handler\n *\n * @typeParam T - Type of the table items for type-safe record access\n * @typeParam C - Type of the context/dependencies returned by context function\n *\n * @example Without context\n * ```typescript\n * export const users = defineTable<User>({\n * pk: { name: \"id\", type: \"string\" },\n * onRecord: async ({ record }) => {\n * console.log(record.new?.name);\n * }\n * });\n * ```\n *\n * @example With context (e.g., Effect runtime)\n * ```typescript\n * export const orders = defineTable<Order, ManagedRuntime<...>>({\n * pk: { name: \"id\", type: \"string\" },\n * context: () => ManagedRuntime.make(\n * Layer.mergeAll(ConfigLive, DbClientLive)\n * ),\n * onRecord: async ({ record, ctx }) => {\n * await ctx.runPromise(processOrder(record));\n * }\n * });\n * ```\n */\nexport type DefineTableOptions<T = Record<string, unknown>, C = undefined, R = void> = TableConfig & {\n /**\n * Factory function to create context/dependencies for onRecord callback.\n * Called once on cold start, result is cached and reused across invocations.\n */\n context?: () => C;\n /** Stream record callback. If omitted, only the table is created (no Lambda) */\n onRecord?: TableRecordFn<T, C, R>;\n /**\n * Callback invoked after all records in the batch are processed.\n * Receives accumulated results from onRecord and list of failures.\n */\n onBatchComplete?: TableBatchCompleteFn<T, C, R>;\n};\n\n/**\n * Internal handler object created by defineTable\n * @internal\n */\nexport type TableHandler<T = Record<string, unknown>, C = undefined, R = void> = {\n readonly __brand: \"effortless-table\";\n readonly config: TableConfig;\n readonly context?: () => C;\n readonly onRecord?: TableRecordFn<T, C, R>;\n readonly onBatchComplete?: TableBatchCompleteFn<T, C, R>;\n};\n\n/**\n * Define a DynamoDB table with optional stream handler\n *\n * Creates:\n * - DynamoDB table with specified key schema\n * - (If onRecord provided) DynamoDB Stream + Lambda + Event Source Mapping\n *\n * @typeParam T - Type of the table items for type-safe record access\n * @typeParam C - Type of the context/dependencies (inferred from context function)\n * @param options - Table configuration, optional context factory, and optional onRecord callback\n * @returns Handler object used by the deployment system\n *\n * @example Table with stream handler (typed)\n * ```typescript\n * type Order = { id: string; amount: number; status: string };\n *\n * export const orders = defineTable<Order>({\n * pk: { name: \"id\", type: \"string\" },\n * streamView: \"NEW_AND_OLD_IMAGES\",\n * batchSize: 10,\n * onRecord: async ({ record }) => {\n * if (record.eventName === \"INSERT\") {\n * console.log(\"New order:\", record.new?.amount);\n * }\n * }\n * });\n * ```\n *\n * @example With Effect runtime context\n * ```typescript\n * export const expenses = defineTable<Expense, typeof expenseRuntime>({\n * pk: { name: \"pk\", type: \"string\" },\n * context: () => ManagedRuntime.make(\n * Layer.mergeAll(ConfigLive, DynamoDBClient.Default())\n * ),\n * onRecord: async ({ record, ctx }) => {\n * await ctx.runPromise(processExpense(record));\n * }\n * });\n * ```\n *\n * @example Table only (no Lambda)\n * ```typescript\n * export const users = defineTable({\n * pk: { name: \"id\", type: \"string\" },\n * sk: { name: \"email\", type: \"string\" }\n * });\n * ```\n */\nexport const defineTable = <T = Record<string, unknown>, C = undefined, R = void>(\n options: DefineTableOptions<T, C, R>\n): TableHandler<T, C, R> => {\n const { onRecord, onBatchComplete, context, ...config } = options;\n return {\n __brand: \"effortless-table\",\n config,\n ...(context ? { context } : {}),\n ...(onRecord ? { onRecord } : {}),\n ...(onBatchComplete ? { onBatchComplete } : {})\n } as TableHandler<T, C, R>;\n};\n"],"mappings":";AA0FO,IAAM,eAAe,CAAC,WAA+C;;;AC+ErE,IAAM,aAAa,CACxB,YACmB;AACnB,QAAM,EAAE,WAAW,SAAS,GAAG,OAAO,IAAI;AAC1C,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B;AAAA,EACF;AACF;;;AC8BO,IAAM,cAAc,CACzB,YAC0B;AAC1B,QAAM,EAAE,UAAU,iBAAiB,SAAS,GAAG,OAAO,IAAI;AAC1D,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,EAC/C;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/handlers/define-http.ts","../src/handlers/define-table.ts","../src/handlers/param.ts"],"sourcesContent":["/**\n * Configuration for an Effortless project.\n *\n * @example\n * ```typescript\n * // effortless.config.ts\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport type EffortlessConfig = {\n /**\n * Project name used for resource naming and tagging.\n * This becomes part of Lambda function names, IAM roles, etc.\n */\n name: string;\n\n /**\n * Default AWS region for all handlers.\n * Can be overridden per-handler or via CLI `--region` flag.\n * @default \"eu-central-1\"\n */\n region?: string;\n\n /**\n * Deployment stage (e.g., \"dev\", \"staging\", \"prod\").\n * Used for resource isolation and tagging.\n * @default \"dev\"\n */\n stage?: string;\n\n /**\n * Glob patterns or directory paths to scan for handlers.\n * Used by `eff deploy` (without file argument) to auto-discover handlers.\n *\n * @example\n * ```typescript\n * // Single directory - scans for all .ts files\n * handlers: \"src\"\n *\n * // Glob patterns\n * handlers: [\"src/**\\/*.ts\", \"lib/**\\/*.handler.ts\"]\n * ```\n */\n handlers?: string | string[];\n\n /**\n * Default settings applied to all handlers unless overridden.\n */\n defaults?: {\n /**\n * Lambda memory in MB.\n * @default 256\n */\n memory?: number;\n\n /**\n * Lambda timeout as a human-readable string.\n * @example \"30 seconds\", \"5 minutes\"\n */\n timeout?: string;\n\n /**\n * Lambda runtime.\n * @default \"nodejs22.x\"\n */\n runtime?: string;\n };\n};\n\n/**\n * Helper function for type-safe configuration.\n * Returns the config object as-is, but provides TypeScript autocompletion.\n *\n * @example\n * ```typescript\n * import { defineConfig } from \"effortless-aws\";\n *\n * export default defineConfig({\n * name: \"my-service\",\n * region: \"eu-central-1\",\n * handlers: \"src\",\n * });\n * ```\n */\nexport const defineConfig = (config: EffortlessConfig): EffortlessConfig => config;\n","import type { Permission } from \"./permissions\";\nimport type { TableHandler } from \"./define-table\";\nimport type { TableClient } from \"../runtime/table-client\";\nimport type { AnyParamRef, ResolveParams } from \"./param\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyTableHandler = TableHandler<any, any, any, any, any>;\n\n/** Maps a deps declaration to resolved runtime client types */\nexport type ResolveDeps<D> = {\n [K in keyof D]: D[K] extends TableHandler<infer T, any, any, any, any> ? TableClient<T> : never;\n};\n\n/** HTTP methods supported by API Gateway */\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"DELETE\" | \"PATCH\";\n\n/**\n * Incoming HTTP request object passed to the handler\n */\nexport type HttpRequest = {\n /** HTTP method (GET, POST, etc.) */\n method: string;\n /** Request path (e.g., \"/users/123\") */\n path: string;\n /** Request headers */\n headers: Record<string, string | undefined>;\n /** Query string parameters */\n query: Record<string, string | undefined>;\n /** Path parameters extracted from route (e.g., {id} -> params.id) */\n params: Record<string, string | undefined>;\n /** Parsed request body (JSON parsed if Content-Type is application/json) */\n body: unknown;\n /** Raw unparsed request body */\n rawBody?: string;\n};\n\n/**\n * HTTP response returned from the handler\n */\nexport type HttpResponse = {\n /** HTTP status code (e.g., 200, 404, 500) */\n status: number;\n /** Response body (will be JSON serialized) */\n body?: unknown;\n /** Response headers */\n headers?: Record<string, string>;\n};\n\n/**\n * Configuration options extracted from DefineHttpOptions (without onRequest callback)\n */\nexport type HttpConfig = {\n /** Handler name. Defaults to export name if not specified */\n name?: string;\n /** HTTP method for the route */\n method: HttpMethod;\n /** Route path (e.g., \"/api/users\", \"/api/users/{id}\") */\n path: string;\n /** Lambda memory in MB (default: 256) */\n memory?: number;\n /** Lambda timeout in seconds (default: 30) */\n timeout?: number;\n /** Additional IAM permissions for the Lambda */\n permissions?: Permission[];\n};\n\n/**\n * Handler function type for HTTP endpoints\n *\n * @typeParam T - Type of the validated request body (from schema function)\n * @typeParam C - Type of the context/dependencies (from context function)\n * @typeParam D - Type of the deps (from deps declaration)\n * @typeParam P - Type of the params (from params declaration)\n */\nexport type HttpHandlerFn<T = undefined, C = undefined, D = undefined, P = undefined> =\n (args: { req: HttpRequest }\n & ([T] extends [undefined] ? {} : { data: T })\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { params: ResolveParams<P> })\n ) => Promise<HttpResponse>;\n\n/**\n * Context factory type — conditional on whether params are declared.\n * Without params: `() => C | Promise<C>`\n * With params: `(args: { params: ResolveParams<P> }) => C | Promise<C>`\n */\ntype ContextFactory<C, P> = [P] extends [undefined]\n ? () => C | Promise<C>\n : (args: { params: ResolveParams<P & {}> }) => C | Promise<C>;\n\n/**\n * Options for defining an HTTP endpoint\n *\n * @typeParam T - Type of the validated request body (inferred from schema function)\n * @typeParam C - Type of the context/dependencies returned by context function\n * @typeParam D - Type of the deps (from deps declaration)\n * @typeParam P - Type of the params (from params declaration)\n */\nexport type DefineHttpOptions<\n T = undefined,\n C = undefined,\n D extends Record<string, AnyTableHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined\n> = HttpConfig & {\n /**\n * Decode/validate function for the request body.\n * Called with the parsed body; should return typed data or throw on validation failure.\n * When provided, the handler receives validated `data` and invalid requests get a 400 response.\n *\n * Works with any validation library:\n * - Effect: `S.decodeUnknownSync(MySchema)`\n * - Zod: `(body) => myZodSchema.parse(body)`\n */\n schema?: (input: unknown) => T;\n /**\n * Error handler called when schema validation or onRequest throws.\n * Receives the error and request, returns an HttpResponse.\n * If not provided, defaults to 400 for validation errors and 500 for handler errors.\n */\n onError?: (error: unknown, req: HttpRequest) => HttpResponse;\n /**\n * Factory function to create context/dependencies for the request handler.\n * Called once on cold start, result is cached and reused across invocations.\n * When params are declared, receives resolved params as argument.\n * Supports both sync and async return values.\n */\n context?: ContextFactory<C, P>;\n /**\n * Dependencies on other handlers (tables, queues, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n * Typed values are injected into the handler via the `params` argument.\n */\n params?: P;\n /** HTTP request handler function */\n onRequest: HttpHandlerFn<T, C, D, P>;\n};\n\n/**\n * Internal handler object created by defineHttp\n * @internal\n */\nexport type HttpHandler<T = undefined, C = undefined, D = undefined, P = undefined> = {\n readonly __brand: \"effortless-http\";\n readonly config: HttpConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown, req: HttpRequest) => HttpResponse;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly context?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly params?: P;\n readonly onRequest: HttpHandlerFn<T, C, D, P>;\n};\n\n/**\n * Define an HTTP endpoint that creates an API Gateway route + Lambda function\n *\n * @typeParam T - Type of the validated request body (inferred from schema function)\n * @typeParam C - Type of the context/dependencies (inferred from context function)\n * @typeParam D - Type of the deps (from deps declaration)\n * @typeParam P - Type of the params (from params declaration)\n * @param options - Configuration, optional schema, optional context factory, and request handler\n * @returns Handler object used by the deployment system\n *\n * @example Basic GET endpoint\n * ```typescript\n * export const hello = defineHttp({\n * method: \"GET\",\n * path: \"/hello\",\n * onRequest: async ({ req }) => ({\n * status: 200,\n * body: { message: \"Hello World!\" }\n * })\n * });\n * ```\n *\n * @example With SSM parameters\n * ```typescript\n * import { param } from \"effortless-aws\";\n *\n * export const api = defineHttp({\n * method: \"GET\",\n * path: \"/orders\",\n * params: {\n * dbUrl: param(\"database-url\"),\n * },\n * context: async ({ params }) => ({\n * pool: createPool(params.dbUrl),\n * }),\n * onRequest: async ({ req, ctx, params }) => ({\n * status: 200,\n * body: { dbUrl: params.dbUrl }\n * })\n * });\n * ```\n */\nexport const defineHttp = <\n T = undefined,\n C = undefined,\n D extends Record<string, AnyTableHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined\n>(\n options: DefineHttpOptions<T, C, D, P>\n): HttpHandler<T, C, D, P> => {\n const { onRequest, onError, context, schema, deps, params, ...config } = options;\n return {\n __brand: \"effortless-http\",\n config,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(context ? { context } : {}),\n ...(deps ? { deps } : {}),\n ...(params ? { params } : {}),\n onRequest\n } as HttpHandler<T, C, D, P>;\n};\n","import type { Permission } from \"./permissions\";\nimport type { TableClient } from \"../runtime/table-client\";\nimport type { AnyParamRef, ResolveParams } from \"./param\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype AnyTableHandler = TableHandler<any, any, any, any, any>;\n\n/** Maps a deps declaration to resolved runtime client types */\ntype ResolveDeps<D> = {\n [K in keyof D]: D[K] extends TableHandler<infer T, any, any, any, any> ? TableClient<T> : never;\n};\n\n/** DynamoDB attribute types for keys */\nexport type KeyType = \"string\" | \"number\" | \"binary\";\n\n/**\n * DynamoDB table key definition\n */\nexport type TableKey = {\n /** Attribute name */\n name: string;\n /** Attribute type */\n type: KeyType;\n};\n\n/** DynamoDB Streams view type - determines what data is captured in stream records */\nexport type StreamView = \"NEW_AND_OLD_IMAGES\" | \"NEW_IMAGE\" | \"OLD_IMAGE\" | \"KEYS_ONLY\";\n\n/**\n * Configuration options extracted from DefineTableOptions (without onRecord/context)\n */\nexport type TableConfig = {\n /** Table/handler name. Defaults to export name if not specified */\n name?: string;\n /** Partition key definition */\n pk: TableKey;\n /** Sort key definition (optional) */\n sk?: TableKey;\n /** DynamoDB billing mode (default: \"PAY_PER_REQUEST\") */\n billingMode?: \"PAY_PER_REQUEST\" | \"PROVISIONED\";\n /** TTL attribute name for automatic item expiration */\n ttlAttribute?: string;\n /** Stream view type - what data to include in stream records (default: \"NEW_AND_OLD_IMAGES\") */\n streamView?: StreamView;\n /** Number of records to process in each Lambda invocation (1-10000, default: 100) */\n batchSize?: number;\n /** Maximum time in seconds to gather records before invoking (0-300, default: 2) */\n batchWindow?: number;\n /** Where to start reading the stream (default: \"LATEST\") */\n startingPosition?: \"LATEST\" | \"TRIM_HORIZON\";\n /** Lambda memory in MB (default: 256) */\n memory?: number;\n /** Lambda timeout in seconds (default: 30) */\n timeout?: number;\n /** Additional IAM permissions for the Lambda */\n permissions?: Permission[];\n};\n\n/**\n * DynamoDB stream record passed to onRecord callback\n *\n * @typeParam T - Type of the table items (new/old values)\n */\nexport type TableRecord<T = Record<string, unknown>> = {\n /** Type of modification: INSERT, MODIFY, or REMOVE */\n eventName: \"INSERT\" | \"MODIFY\" | \"REMOVE\";\n /** New item value (present for INSERT and MODIFY) */\n new?: T;\n /** Old item value (present for MODIFY and REMOVE) */\n old?: T;\n /** Primary key of the affected item */\n keys: Record<string, unknown>;\n /** Sequence number for ordering */\n sequenceNumber?: string;\n /** Approximate timestamp when the modification occurred */\n timestamp?: number;\n};\n\n/**\n * Information about a failed record during batch processing\n *\n * @typeParam T - Type of the table items\n */\nexport type FailedRecord<T = Record<string, unknown>> = {\n /** The record that failed to process */\n record: TableRecord<T>;\n /** The error that occurred */\n error: unknown;\n};\n\n/**\n * Context factory type — conditional on whether params are declared.\n * Without params: `() => C | Promise<C>`\n * With params: `(args: { params: ResolveParams<P> }) => C | Promise<C>`\n */\ntype ContextFactory<C, P> = [P] extends [undefined]\n ? () => C | Promise<C>\n : (args: { params: ResolveParams<P & {}> }) => C | Promise<C>;\n\n/**\n * Callback function type for processing a single DynamoDB stream record\n */\nexport type TableRecordFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined> =\n (args: { record: TableRecord<T>; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { params: ResolveParams<P> })\n ) => Promise<R>;\n\n/**\n * Callback function type for processing accumulated batch results\n */\nexport type TableBatchCompleteFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined> =\n (args: { results: R[]; failures: FailedRecord<T>[]; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { params: ResolveParams<P> })\n ) => Promise<void>;\n\n/**\n * Callback function type for processing all records in a batch at once\n */\nexport type TableBatchFn<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined> =\n (args: { records: TableRecord<T>[]; table: TableClient<T> }\n & ([C] extends [undefined] ? {} : { ctx: C })\n & ([D] extends [undefined] ? {} : { deps: ResolveDeps<D> })\n & ([P] extends [undefined] ? {} : { params: ResolveParams<P> })\n ) => Promise<void>;\n\n/** Base options shared by all defineTable variants */\ntype DefineTableBase<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined> = TableConfig & {\n /**\n * Decode/validate function for stream record items (new/old images).\n * Called with the unmarshalled DynamoDB item; should return typed data or throw on validation failure.\n * When provided, T is inferred from the return type — no need to specify generic parameters.\n */\n schema?: (input: unknown) => T;\n /**\n * Error handler called when onRecord, onBatch, or onBatchComplete throws.\n * Receives the error. If not provided, defaults to `console.error`.\n */\n onError?: (error: unknown) => void;\n /**\n * Factory function to create context/dependencies for callbacks.\n * Called once on cold start, result is cached and reused across invocations.\n * When params are declared, receives resolved params as argument.\n * Supports both sync and async return values.\n */\n context?: ContextFactory<C, P>;\n /**\n * Dependencies on other handlers (tables, queues, etc.).\n * Typed clients are injected into the handler via the `deps` argument.\n */\n deps?: D;\n /**\n * SSM Parameter Store parameters.\n * Declare with `param()` helper. Values are fetched and cached at cold start.\n * Typed values are injected into the handler via the `params` argument.\n */\n params?: P;\n};\n\n/** Per-record processing: onRecord with optional onBatchComplete */\ntype DefineTableWithOnRecord<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined> = DefineTableBase<T, C, D, P> & {\n onRecord: TableRecordFn<T, C, R, D, P>;\n onBatchComplete?: TableBatchCompleteFn<T, C, R, D, P>;\n onBatch?: never;\n};\n\n/** Batch processing: onBatch processes all records at once */\ntype DefineTableWithOnBatch<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined> = DefineTableBase<T, C, D, P> & {\n onBatch: TableBatchFn<T, C, D, P>;\n onRecord?: never;\n onBatchComplete?: never;\n};\n\n/** Resource-only: no handler, just creates the table */\ntype DefineTableResourceOnly<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined> = DefineTableBase<T, C, D, P> & {\n onRecord?: never;\n onBatch?: never;\n onBatchComplete?: never;\n};\n\nexport type DefineTableOptions<\n T = Record<string, unknown>,\n C = undefined,\n R = void,\n D extends Record<string, AnyTableHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined\n> =\n | DefineTableWithOnRecord<T, C, R, D, P>\n | DefineTableWithOnBatch<T, C, D, P>\n | DefineTableResourceOnly<T, C, D, P>;\n\n/**\n * Internal handler object created by defineTable\n * @internal\n */\nexport type TableHandler<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined> = {\n readonly __brand: \"effortless-table\";\n readonly config: TableConfig;\n readonly schema?: (input: unknown) => T;\n readonly onError?: (error: unknown) => void;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n readonly context?: (...args: any[]) => C | Promise<C>;\n readonly deps?: D;\n readonly params?: P;\n readonly onRecord?: TableRecordFn<T, C, R, D, P>;\n readonly onBatchComplete?: TableBatchCompleteFn<T, C, R, D, P>;\n readonly onBatch?: TableBatchFn<T, C, D, P>;\n};\n\n/**\n * Define a DynamoDB table with optional stream handler\n *\n * Creates:\n * - DynamoDB table with specified key schema\n * - (If onRecord or onBatch provided) DynamoDB Stream + Lambda + Event Source Mapping\n *\n * @example Table with stream handler (typed)\n * ```typescript\n * type Order = { id: string; amount: number; status: string };\n *\n * export const orders = defineTable<Order>({\n * pk: { name: \"id\", type: \"string\" },\n * streamView: \"NEW_AND_OLD_IMAGES\",\n * batchSize: 10,\n * onRecord: async ({ record }) => {\n * if (record.eventName === \"INSERT\") {\n * console.log(\"New order:\", record.new?.amount);\n * }\n * }\n * });\n * ```\n *\n * @example Table only (no Lambda)\n * ```typescript\n * export const users = defineTable({\n * pk: { name: \"id\", type: \"string\" },\n * sk: { name: \"email\", type: \"string\" }\n * });\n * ```\n */\nexport const defineTable = <\n T = Record<string, unknown>,\n C = undefined,\n R = void,\n D extends Record<string, AnyTableHandler> | undefined = undefined,\n P extends Record<string, AnyParamRef> | undefined = undefined\n>(\n options: DefineTableOptions<T, C, R, D, P>\n): TableHandler<T, C, R, D, P> => {\n const { onRecord, onBatchComplete, onBatch, onError, schema, context, deps, params, ...config } = options;\n return {\n __brand: \"effortless-table\",\n config,\n ...(schema ? { schema } : {}),\n ...(onError ? { onError } : {}),\n ...(context ? { context } : {}),\n ...(deps ? { deps } : {}),\n ...(params ? { params } : {}),\n ...(onRecord ? { onRecord } : {}),\n ...(onBatchComplete ? { onBatchComplete } : {}),\n ...(onBatch ? { onBatch } : {})\n } as TableHandler<T, C, R, D, P>;\n};\n","// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type AnyParamRef = ParamRef<any>;\n\n/**\n * Reference to an SSM Parameter Store parameter.\n *\n * @typeParam T - The resolved type after optional transform (default: string)\n */\nexport type ParamRef<T = string> = {\n readonly __brand: \"effortless-param\";\n readonly key: string;\n readonly transform?: (raw: string) => T;\n};\n\n/**\n * Maps a params declaration to resolved value types.\n *\n * @typeParam P - Record of param names to ParamRef instances\n */\nexport type ResolveParams<P> = {\n [K in keyof P]: P[K] extends ParamRef<infer T> ? T : never;\n};\n\n/**\n * Declare an SSM Parameter Store parameter.\n *\n * The key is combined with project and stage at deploy time to form the full\n * SSM path: `/${project}/${stage}/${key}`.\n *\n * @param key - Parameter key (e.g., \"database-url\")\n * @param transform - Optional function to transform the raw string value\n * @returns A ParamRef used by the deployment and runtime systems\n *\n * @example Simple string parameter\n * ```typescript\n * params: {\n * dbUrl: param(\"database-url\"),\n * }\n * ```\n *\n * @example With transform (e.g., TOML parsing)\n * ```typescript\n * import TOML from \"smol-toml\";\n *\n * params: {\n * config: param(\"app-config\", TOML.parse),\n * }\n * ```\n */\nexport const param = <T = string>(\n key: string,\n transform?: (raw: string) => T\n): ParamRef<T> => ({\n __brand: \"effortless-param\",\n key,\n ...(transform ? { transform } : {}),\n});\n"],"mappings":";AA0FO,IAAM,eAAe,CAAC,WAA+C;;;AC+GrE,IAAM,aAAa,CAMxB,YAC4B;AAC5B,QAAM,EAAE,WAAW,SAAS,SAAS,QAAQ,MAAM,QAAQ,GAAG,OAAO,IAAI;AACzE,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B;AAAA,EACF;AACF;;;ACuBO,IAAM,cAAc,CAOzB,YACgC;AAChC,QAAM,EAAE,UAAU,iBAAiB,SAAS,SAAS,QAAQ,SAAS,MAAM,QAAQ,GAAG,OAAO,IAAI;AAClG,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;AAAA,IAC3B,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,kBAAkB,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC7C,GAAI,UAAU,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC/B;AACF;;;ACxNO,IAAM,QAAQ,CACnB,KACA,eACiB;AAAA,EACjB,SAAS;AAAA,EACT;AAAA,EACA,GAAI,YAAY,EAAE,UAAU,IAAI,CAAC;AACnC;","names":[]}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDeps,
|
|
3
|
+
buildParams
|
|
4
|
+
} from "../chunk-I5TS7O5S.js";
|
|
5
|
+
|
|
6
|
+
// src/runtime/wrap-http.ts
|
|
7
|
+
var parseBody = (body, isBase64) => {
|
|
8
|
+
if (!body) return void 0;
|
|
9
|
+
const decoded = isBase64 ? Buffer.from(body, "base64").toString("utf-8") : body;
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(decoded);
|
|
12
|
+
} catch {
|
|
13
|
+
return decoded;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var wrapHttp = (handler) => {
|
|
17
|
+
let ctx = null;
|
|
18
|
+
let resolvedDeps;
|
|
19
|
+
let resolvedParams = null;
|
|
20
|
+
const getDeps = () => resolvedDeps ??= buildDeps(handler.deps);
|
|
21
|
+
const getParams = async () => {
|
|
22
|
+
if (resolvedParams !== null) return resolvedParams;
|
|
23
|
+
resolvedParams = await buildParams(handler.params);
|
|
24
|
+
return resolvedParams;
|
|
25
|
+
};
|
|
26
|
+
const getCtx = async () => {
|
|
27
|
+
if (ctx !== null) return ctx;
|
|
28
|
+
if (handler.context) {
|
|
29
|
+
const params = await getParams();
|
|
30
|
+
ctx = params ? await handler.context({ params }) : await handler.context();
|
|
31
|
+
}
|
|
32
|
+
return ctx;
|
|
33
|
+
};
|
|
34
|
+
return async (event) => {
|
|
35
|
+
const req = {
|
|
36
|
+
method: event.requestContext?.http?.method ?? event.httpMethod ?? "GET",
|
|
37
|
+
path: event.requestContext?.http?.path ?? event.path ?? "/",
|
|
38
|
+
headers: event.headers ?? {},
|
|
39
|
+
query: event.queryStringParameters ?? {},
|
|
40
|
+
params: event.pathParameters ?? {},
|
|
41
|
+
body: parseBody(event.body, event.isBase64Encoded ?? false),
|
|
42
|
+
rawBody: event.body
|
|
43
|
+
};
|
|
44
|
+
const toResult = (r) => ({
|
|
45
|
+
statusCode: r.status,
|
|
46
|
+
headers: { "Content-Type": "application/json", ...r.headers },
|
|
47
|
+
body: JSON.stringify(r.body)
|
|
48
|
+
});
|
|
49
|
+
const defaultError = (error, status) => ({
|
|
50
|
+
statusCode: status,
|
|
51
|
+
headers: { "Content-Type": "application/json" },
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
error: status === 400 ? "Validation failed" : "Internal server error",
|
|
54
|
+
details: error instanceof Error ? error.message : String(error)
|
|
55
|
+
})
|
|
56
|
+
});
|
|
57
|
+
const args = { req };
|
|
58
|
+
if (handler.schema) {
|
|
59
|
+
try {
|
|
60
|
+
args.data = handler.schema(req.body);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return handler.onError ? toResult(handler.onError(error, req)) : defaultError(error, 400);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (handler.context) {
|
|
66
|
+
args.ctx = await getCtx();
|
|
67
|
+
}
|
|
68
|
+
const deps = getDeps();
|
|
69
|
+
if (deps) {
|
|
70
|
+
args.deps = deps;
|
|
71
|
+
}
|
|
72
|
+
const params = await getParams();
|
|
73
|
+
if (params) {
|
|
74
|
+
args.params = params;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const response = await handler.onRequest(args);
|
|
78
|
+
return toResult(response);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return handler.onError ? toResult(handler.onError(error, req)) : defaultError(error, 500);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
export {
|
|
85
|
+
wrapHttp
|
|
86
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildDeps,
|
|
3
|
+
buildParams,
|
|
4
|
+
createTableClient
|
|
5
|
+
} from "../chunk-I5TS7O5S.js";
|
|
6
|
+
|
|
7
|
+
// src/runtime/wrap-table-stream.ts
|
|
8
|
+
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
9
|
+
var parseRecords = (rawRecords, schema) => {
|
|
10
|
+
const records = [];
|
|
11
|
+
const sequenceNumbers = /* @__PURE__ */ new Map();
|
|
12
|
+
const decode = schema ?? ((x) => x);
|
|
13
|
+
for (const rawRecord of rawRecords) {
|
|
14
|
+
if (!rawRecord.dynamodb?.Keys) continue;
|
|
15
|
+
const newImage = rawRecord.dynamodb?.NewImage ? unmarshall(rawRecord.dynamodb.NewImage) : void 0;
|
|
16
|
+
const oldImage = rawRecord.dynamodb?.OldImage ? unmarshall(rawRecord.dynamodb.OldImage) : void 0;
|
|
17
|
+
const record = {
|
|
18
|
+
eventName: rawRecord.eventName,
|
|
19
|
+
new: newImage !== void 0 ? decode(newImage) : void 0,
|
|
20
|
+
old: oldImage !== void 0 ? decode(oldImage) : void 0,
|
|
21
|
+
keys: unmarshall(rawRecord.dynamodb.Keys),
|
|
22
|
+
sequenceNumber: rawRecord.dynamodb?.SequenceNumber,
|
|
23
|
+
timestamp: rawRecord.dynamodb?.ApproximateCreationDateTime
|
|
24
|
+
};
|
|
25
|
+
records.push(record);
|
|
26
|
+
if (rawRecord.dynamodb?.SequenceNumber) {
|
|
27
|
+
sequenceNumbers.set(record, rawRecord.dynamodb.SequenceNumber);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { records, sequenceNumbers };
|
|
31
|
+
};
|
|
32
|
+
var ENV_TABLE_SELF = "EFF_TABLE_SELF";
|
|
33
|
+
var wrapTableStream = (handler) => {
|
|
34
|
+
if (!handler.onRecord && !handler.onBatch) {
|
|
35
|
+
throw new Error("wrapTableStream requires a handler with onRecord or onBatch defined");
|
|
36
|
+
}
|
|
37
|
+
const handleError = handler.onError ?? ((e) => console.error(e));
|
|
38
|
+
let ctx = null;
|
|
39
|
+
let resolvedDeps;
|
|
40
|
+
let resolvedParams = null;
|
|
41
|
+
const getDeps = () => resolvedDeps ??= buildDeps(handler.deps);
|
|
42
|
+
const getParams = async () => {
|
|
43
|
+
if (resolvedParams !== null) return resolvedParams;
|
|
44
|
+
resolvedParams = await buildParams(handler.params);
|
|
45
|
+
return resolvedParams;
|
|
46
|
+
};
|
|
47
|
+
const getCtx = async () => {
|
|
48
|
+
if (ctx !== null) return ctx;
|
|
49
|
+
if (handler.context) {
|
|
50
|
+
const params = await getParams();
|
|
51
|
+
ctx = params ? await handler.context({ params }) : await handler.context();
|
|
52
|
+
}
|
|
53
|
+
return ctx;
|
|
54
|
+
};
|
|
55
|
+
let selfClient = null;
|
|
56
|
+
const getSelfClient = () => {
|
|
57
|
+
if (selfClient) return selfClient;
|
|
58
|
+
const tableName = process.env[ENV_TABLE_SELF];
|
|
59
|
+
if (!tableName) return void 0;
|
|
60
|
+
selfClient = createTableClient(tableName);
|
|
61
|
+
return selfClient;
|
|
62
|
+
};
|
|
63
|
+
const commonArgs = async () => {
|
|
64
|
+
const args = {};
|
|
65
|
+
if (handler.context) args.ctx = await getCtx();
|
|
66
|
+
const deps = getDeps();
|
|
67
|
+
if (deps) args.deps = deps;
|
|
68
|
+
const params = await getParams();
|
|
69
|
+
if (params) args.params = params;
|
|
70
|
+
const table = getSelfClient();
|
|
71
|
+
if (table) args.table = table;
|
|
72
|
+
return args;
|
|
73
|
+
};
|
|
74
|
+
return async (event) => {
|
|
75
|
+
const rawRecords = event.Records ?? [];
|
|
76
|
+
let records;
|
|
77
|
+
let sequenceNumbers;
|
|
78
|
+
try {
|
|
79
|
+
({ records, sequenceNumbers } = parseRecords(rawRecords, handler.schema));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
handleError(error);
|
|
82
|
+
return {
|
|
83
|
+
batchItemFailures: rawRecords.map((r) => r.dynamodb?.SequenceNumber).filter((s) => !!s).map((seq) => ({ itemIdentifier: seq }))
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const batchItemFailures = [];
|
|
87
|
+
if (handler.onBatch) {
|
|
88
|
+
try {
|
|
89
|
+
const onBatch = handler.onBatch;
|
|
90
|
+
await onBatch({ records, ...await commonArgs() });
|
|
91
|
+
} catch (error) {
|
|
92
|
+
handleError(error);
|
|
93
|
+
for (const record of records) {
|
|
94
|
+
const seq = sequenceNumbers.get(record);
|
|
95
|
+
if (seq) {
|
|
96
|
+
batchItemFailures.push({ itemIdentifier: seq });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { batchItemFailures };
|
|
101
|
+
}
|
|
102
|
+
const results = [];
|
|
103
|
+
const failures = [];
|
|
104
|
+
const onRecord = handler.onRecord;
|
|
105
|
+
const shared = await commonArgs();
|
|
106
|
+
for (const record of records) {
|
|
107
|
+
try {
|
|
108
|
+
const result = await onRecord({ record, ...shared });
|
|
109
|
+
if (result !== void 0) {
|
|
110
|
+
results.push(result);
|
|
111
|
+
}
|
|
112
|
+
} catch (error) {
|
|
113
|
+
handleError(error);
|
|
114
|
+
failures.push({ record, error });
|
|
115
|
+
const seq = sequenceNumbers.get(record);
|
|
116
|
+
if (seq) {
|
|
117
|
+
batchItemFailures.push({ itemIdentifier: seq });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (handler.onBatchComplete) {
|
|
122
|
+
try {
|
|
123
|
+
const onBatchComplete = handler.onBatchComplete;
|
|
124
|
+
await onBatchComplete({ results, failures, ...shared });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
handleError(error);
|
|
127
|
+
for (const record of records) {
|
|
128
|
+
const seq = sequenceNumbers.get(record);
|
|
129
|
+
if (seq) {
|
|
130
|
+
const alreadyFailed = batchItemFailures.some((f) => f.itemIdentifier === seq);
|
|
131
|
+
if (!alreadyFailed) {
|
|
132
|
+
batchItemFailures.push({ itemIdentifier: seq });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { batchItemFailures };
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
export {
|
|
142
|
+
wrapTableStream
|
|
143
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effortless-aws",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Code-first AWS Lambda framework",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"@aws-sdk/client-iam": "^3.975.0",
|
|
38
38
|
"@aws-sdk/client-lambda": "^3.975.0",
|
|
39
39
|
"@aws-sdk/client-resource-groups-tagging-api": "^3.975.0",
|
|
40
|
+
"@aws-sdk/client-ssm": "^3.985.0",
|
|
40
41
|
"@aws-sdk/util-dynamodb": "^3.975.0",
|
|
41
42
|
"@vercel/nft": "^1.3.0",
|
|
42
43
|
"archiver": "^7.0.1",
|
|
@@ -64,18 +65,12 @@
|
|
|
64
65
|
"vite-tsconfig-paths": "^6.0.5",
|
|
65
66
|
"vitest": "^4.0.18"
|
|
66
67
|
},
|
|
67
|
-
"peerDependencies": {
|
|
68
|
-
"effect": "^3.19.14"
|
|
69
|
-
},
|
|
70
68
|
"scripts": {
|
|
71
69
|
"gen": "gen-aws-sdk",
|
|
72
70
|
"prebuild": "pnpm run gen",
|
|
73
71
|
"build": "tsup",
|
|
74
72
|
"typecheck": "tsc --noEmit",
|
|
75
73
|
"test": "vitest run",
|
|
76
|
-
"
|
|
77
|
-
"pack": "pnpm build && rm -rf dist-pack && mkdir -p dist-pack && pnpm pack --pack-destination dist-pack",
|
|
78
|
-
"changeset": "changeset",
|
|
79
|
-
"release": "changeset publish"
|
|
74
|
+
"link": "pnpm build && pnpm link --global"
|
|
80
75
|
}
|
|
81
76
|
}
|