effortless-aws 0.28.0 → 0.30.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/dist/index.d.ts CHANGED
@@ -95,6 +95,8 @@ type EffortlessConfig = {
95
95
  */
96
96
  declare const defineConfig: (config: EffortlessConfig) => EffortlessConfig;
97
97
 
98
+ /** Generator spec for auto-creating secrets at deploy time. */
99
+ type GenerateSpec = `hex:${number}` | `base64:${number}` | "uuid";
98
100
  type AwsService = "dynamodb" | "s3" | "sqs" | "sns" | "ses" | "ssm" | "lambda" | "events" | "secretsmanager" | "cognito-idp" | "logs";
99
101
  type Permission = `${AwsService}:${string}` | (string & {});
100
102
  /**
@@ -143,7 +145,7 @@ type AnySecretRef = SecretRef<any>;
143
145
  type SecretRef<T = string> = {
144
146
  readonly __brand: "effortless-secret";
145
147
  readonly key?: string;
146
- readonly generate?: () => string;
148
+ readonly generate?: GenerateSpec;
147
149
  readonly transform?: (raw: string) => T;
148
150
  };
149
151
  /**
@@ -155,76 +157,46 @@ type SecretRef<T = string> = {
155
157
  type ResolveConfig<P> = {
156
158
  [K in keyof P]: P[K] extends SecretRef<infer T> ? T : string;
157
159
  };
158
- /** Options for `secret()` without a transform. */
159
- type SecretOptions = {
160
+ /** Options for `defineSecret()` without a transform. */
161
+ type DefineSecretOptions = {
160
162
  /** Custom SSM key (default: derived from config property name in kebab-case) */
161
163
  key?: string;
162
- /** Generator function called at deploy time if the SSM parameter doesn't exist yet */
163
- generate?: () => string;
164
+ /** Generator spec for auto-creating the secret at deploy time: `"hex:32"`, `"base64:32"`, `"uuid"` */
165
+ generate?: GenerateSpec;
164
166
  };
165
- /** Options for `secret()` with a transform. */
166
- type SecretOptionsWithTransform<T> = SecretOptions & {
167
+ /** Options for `defineSecret()` with a transform. */
168
+ type DefineSecretOptionsWithTransform<T> = DefineSecretOptions & {
167
169
  /** Transform the raw SSM string value into a typed value */
168
170
  transform: (raw: string) => T;
169
171
  };
170
- /**
171
- * Declare an SSM Parameter Store secret.
172
- *
173
- * The SSM key is derived from the config property name (camelCase kebab-case)
174
- * unless overridden with `key`. The full SSM path is `/${project}/${stage}/${key}`.
175
- *
176
- * @param options - Optional configuration (key override, generator, transform)
177
- * @returns A SecretRef used by the deployment and runtime systems
178
- *
179
- * @example Simple secret
180
- * ```typescript
181
- * config: {
182
- * dbUrl: secret(),
183
- * // → SSM path: /${project}/${stage}/db-url
184
- * }
185
- * ```
186
- *
187
- * @example Auto-generated secret
188
- * ```typescript
189
- * config: {
190
- * authSecret: secret({ generate: generateHex(64) }),
191
- * // → auto-creates SSM param if missing
192
- * }
193
- * ```
194
- *
195
- * @example With transform
196
- * ```typescript
197
- * config: {
198
- * appConfig: secret({ transform: TOML.parse }),
199
- * }
200
- * ```
201
- */
202
- declare function secret(): SecretRef<string>;
203
- declare function secret(options: SecretOptions): SecretRef<string>;
204
- declare function secret<T>(options: SecretOptionsWithTransform<T>): SecretRef<T>;
205
- /**
206
- * Returns a generator that produces a random hex string.
207
- * @param bytes - Number of random bytes (output will be 2x this length in hex chars)
208
- */
209
- declare const generateHex: (bytes: number) => () => string;
210
- /**
211
- * Returns a generator that produces a random base64url string.
212
- * @param bytes - Number of random bytes
213
- */
214
- declare const generateBase64: (bytes: number) => () => string;
215
- /**
216
- * Returns a generator that produces a random UUID v4.
217
- */
218
- declare const generateUuid: () => () => string;
172
+ /** The defineSecret helper function type, injected into config callbacks. */
173
+ type DefineSecretFn = {
174
+ (): SecretRef<string>;
175
+ (options: DefineSecretOptions): SecretRef<string>;
176
+ <T>(options: DefineSecretOptionsWithTransform<T>): SecretRef<T>;
177
+ };
178
+ /** Helpers injected into the `config` callback. */
179
+ type ConfigHelpers = {
180
+ defineSecret: DefineSecretFn;
181
+ };
182
+ /** Config factory: a function receiving helpers and returning a record of SecretRefs. */
183
+ type ConfigFactory<P> = (helpers: ConfigHelpers) => P;
184
+ /** The `defineSecret` implementation, passed to config callbacks. */
185
+ declare const defineSecret: DefineSecretFn;
186
+ /** @deprecated Use `defineSecret()` inside a config callback instead. */
187
+ declare const secret: DefineSecretFn;
219
188
  /** @deprecated Use `SecretRef` instead */
220
189
  type ParamRef<T = string> = SecretRef<T>;
221
190
  /** @deprecated Use `AnySecretRef` instead */
222
191
  type AnyParamRef = AnySecretRef;
223
- /**
224
- * @deprecated Use `secret()` instead. `param("key")` is equivalent to `secret({ key: "key" })`.
225
- */
226
- declare function param(key: string): SecretRef<string>;
227
- declare function param<T>(key: string, transform: (raw: string) => T): SecretRef<T>;
192
+ /** @deprecated Use `defineSecret()` instead. */
193
+ declare const param: <T = string>(key: string, transform?: (raw: string) => T) => SecretRef<T>;
194
+ /** @deprecated Use `defineSecret({ generate: "hex:N" })` instead. */
195
+ declare const generateHex: (bytes: number) => string;
196
+ /** @deprecated Use `defineSecret({ generate: "base64:N" })` instead. */
197
+ declare const generateBase64: (bytes: number) => string;
198
+ /** @deprecated Use `defineSecret({ generate: "uuid" })` instead. */
199
+ declare const generateUuid: () => string;
228
200
  /**
229
201
  * DynamoDB table key (always pk + sk strings in single-table design).
230
202
  */
@@ -281,113 +253,8 @@ type PutInput<T> = {
281
253
  */
282
254
  declare function unsafeAs<T>(): (input: unknown) => T;
283
255
 
284
- /**
285
- * Sort key condition for TableClient.query()
286
- */
287
- type SkCondition = string | {
288
- begins_with: string;
289
- } | {
290
- gt: string;
291
- } | {
292
- gte: string;
293
- } | {
294
- lt: string;
295
- } | {
296
- lte: string;
297
- } | {
298
- between: [string, string];
299
- };
300
- /**
301
- * Query parameters for TableClient.query()
302
- */
303
- type QueryParams = {
304
- /** Partition key value */
305
- pk: string;
306
- /** Optional sort key condition */
307
- sk?: SkCondition;
308
- /** Maximum number of items to return */
309
- limit?: number;
310
- /** Sort order (true = ascending, false = descending) */
311
- scanIndexForward?: boolean;
312
- };
313
- /**
314
- * Query parameters for TableClient.queryByTag() — cross-partition query via GSI.
315
- * Uses the built-in `tag-pk-index` GSI (tag as partition key, pk as sort key).
316
- */
317
- type QueryByTagParams = {
318
- /** Tag value (GSI partition key) — the entity type discriminant */
319
- tag: string;
320
- /** Optional pk condition (GSI sort key) */
321
- pk?: SkCondition;
322
- /** Maximum number of items to return */
323
- limit?: number;
324
- /** Sort order (true = ascending, false = descending) */
325
- scanIndexForward?: boolean;
326
- };
327
- /** Extract keys of T whose values are arrays */
328
- type ArrayKeys<T> = {
329
- [K in keyof T]: T[K] extends unknown[] ? K : never;
330
- }[keyof T];
331
- /** Extract keys of T whose values are numbers */
332
- type NumberKeys<T> = {
333
- [K in keyof T]: T[K] extends number ? K : never;
334
- }[keyof T];
335
- /**
336
- * Update actions for TableClient.update()
337
- *
338
- * `set`, `append`, and `remove` target fields inside the `data` attribute.
339
- * Effortless auto-prefixes `data.` in the DynamoDB expression.
340
- *
341
- * @typeParam T - Type of the domain data (the `data` attribute)
342
- */
343
- type UpdateActions<T> = {
344
- /** Set domain data fields (inside `data` attribute) */
345
- set?: Partial<T>;
346
- /** Atomically increment/decrement numeric fields inside `data` (use negative values to decrement) */
347
- increment?: Pick<Partial<T>, NumberKeys<T>>;
348
- /** Append elements to list fields inside `data` (creates the list if it doesn't exist) */
349
- append?: Pick<Partial<T>, ArrayKeys<T>>;
350
- /** Remove fields from `data` */
351
- remove?: (keyof T)[];
352
- /** Update the top-level `tag` attribute */
353
- tag?: string;
354
- /** Update TTL (set number or null to remove) */
355
- ttl?: number | null;
356
- };
357
- /**
358
- * Typed DynamoDB table client for single-table design.
359
- *
360
- * All items follow the `{ pk, sk, tag, data, ttl? }` structure.
361
- * `T` is the domain data type stored in the `data` attribute.
362
- *
363
- * @typeParam T - Type of the domain data
364
- */
365
- /**
366
- * Options for `put()` operation.
367
- */
368
- type PutOptions = {
369
- /** When true, the put fails if an item with the same pk+sk already exists. */
370
- ifNotExists?: boolean;
371
- };
372
- type TableClient<T = Record<string, unknown>> = {
373
- /** Put an item. Tag is auto-extracted from `data[tagField]`. Use `ifNotExists` to prevent overwrites. */
374
- put(item: PutInput<T>, options?: PutOptions): Promise<void>;
375
- /** Get an item by pk + sk */
376
- get(key: TableKey): Promise<TableItem<T> | undefined>;
377
- /** Delete an item by pk + sk */
378
- delete(key: TableKey): Promise<void>;
379
- /** Update domain data fields without reading the full item */
380
- update(key: TableKey, actions: UpdateActions<T>): Promise<void>;
381
- /** Query by partition key with optional sort key condition */
382
- query(params: QueryParams): Promise<TableItem<T>[]>;
383
- /** Query by tag across all partitions via GSI (tag-pk-index). */
384
- queryByTag(params: QueryByTagParams): Promise<TableItem<T>[]>;
385
- /** The underlying DynamoDB table name */
386
- tableName: string;
387
- };
388
-
389
256
  /** HTTP methods supported by Lambda Function URLs */
390
- type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY";
257
+ type HttpMethod$1 = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY";
391
258
  /** Short content-type aliases for common response formats */
392
259
  type ContentType = "json" | "html" | "text" | "css" | "js" | "xml" | "csv" | "svg";
393
260
  /**
@@ -439,13 +306,6 @@ type HttpResponse = {
439
306
  */
440
307
  binary?: boolean;
441
308
  };
442
- /** Response helpers for defineApi handlers */
443
- declare const result: {
444
- /** Return a JSON response */
445
- json: (body: unknown, status?: number) => HttpResponse;
446
- /** Return a binary response. Accepts a Buffer and converts to base64 automatically. */
447
- binary: (body: Buffer, contentType: string, headers?: Record<string, string>) => HttpResponse;
448
- };
449
309
  /** Stream helper injected into route args when `stream: true` is set on defineApi */
450
310
  type ResponseStream = {
451
311
  /** Write a raw string chunk to the response stream */
@@ -493,27 +353,17 @@ type BucketClient = {
493
353
  bucketName: string;
494
354
  };
495
355
 
496
- /**
497
- * Common conditional args injected into handler callbacks.
498
- * Resolves ctx, deps, config, and files based on whether each generic is defined.
499
- * @internal
500
- */
501
- type HandlerArgs<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = ([C] extends [undefined] ? {} : {
502
- ctx: C;
503
- }) & ([D] extends [undefined] ? {} : {
504
- deps: ResolveDeps<D>;
505
- }) & ([P] extends [undefined] ? {} : {
506
- config: ResolveConfig<P>;
507
- }) & ([S] extends [undefined] ? {} : {
508
- files: StaticFiles;
509
- });
510
-
511
356
  /**
512
357
  * Configuration options for defineBucket.
513
358
  */
514
359
  type BucketConfig = {
515
360
  /** Lambda function settings (memory, timeout, permissions, etc.) */
516
- lambda?: LambdaWithPermissions;
361
+ lambda?: {
362
+ memory?: number;
363
+ timeout?: Duration;
364
+ logLevel?: LogLevel;
365
+ permissions?: Permission[];
366
+ };
517
367
  /** S3 key prefix filter for event notifications (e.g., "uploads/") */
518
368
  prefix?: string;
519
369
  /** S3 key suffix filter for event notifications (e.g., ".jpg") */
@@ -536,80 +386,30 @@ type BucketEvent = {
536
386
  /** S3 bucket name */
537
387
  bucketName: string;
538
388
  };
389
+ /** Spread ctx into callback args (empty when no setup) */
390
+ type SpreadCtx$3<C> = [C] extends [undefined] ? {} : C & {};
391
+ /** Setup factory — receives bucket/deps/config/files based on what was declared */
392
+ type SetupArgs$3<D, P, HasFiles extends boolean> = {
393
+ bucket: BucketClient;
394
+ } & ([D] extends [undefined] ? {} : {
395
+ deps: ResolveDeps<D>;
396
+ }) & ([P] extends [undefined] ? {} : {
397
+ config: ResolveConfig<P & {}>;
398
+ }) & (HasFiles extends true ? {
399
+ files: StaticFiles;
400
+ } : {});
539
401
  /**
540
402
  * Callback function type for S3 ObjectCreated events
541
403
  */
542
- type BucketObjectCreatedFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
404
+ type BucketObjectCreatedFn<C = undefined> = (args: {
543
405
  event: BucketEvent;
544
- bucket: BucketClient;
545
- } & HandlerArgs<C, D, P, S>) => Promise<void>;
406
+ } & SpreadCtx$3<C>) => Promise<void>;
546
407
  /**
547
408
  * Callback function type for S3 ObjectRemoved events
548
409
  */
549
- type BucketObjectRemovedFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
410
+ type BucketObjectRemovedFn<C = undefined> = (args: {
550
411
  event: BucketEvent;
551
- bucket: BucketClient;
552
- } & HandlerArgs<C, D, P, S>) => Promise<void>;
553
- /**
554
- * Setup factory type for bucket handlers.
555
- * Always receives `bucket: BucketClient` (self-client for the handler's own bucket).
556
- * Also receives `deps` and/or `config` when declared.
557
- */
558
- type SetupFactory$3<C, D, P, S extends string[] | undefined = undefined> = (args: {
559
- bucket: BucketClient;
560
- } & ([D] extends [undefined] ? {} : {
561
- deps: ResolveDeps<D>;
562
- }) & ([P] extends [undefined] ? {} : {
563
- config: ResolveConfig<P & {}>;
564
- }) & ([S] extends [undefined] ? {} : {
565
- files: StaticFiles;
566
- })) => C | Promise<C>;
567
- /** Base options shared by all defineBucket variants */
568
- type DefineBucketBase<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = BucketConfig & {
569
- /**
570
- * Error handler called when onObjectCreated or onObjectRemoved throws.
571
- * If not provided, defaults to `console.error`.
572
- */
573
- onError?: (args: {
574
- error: unknown;
575
- } & HandlerArgs<C, D, P, S>) => void;
576
- /** Called after each invocation completes, right before Lambda freezes the process */
577
- onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<void>;
578
- /**
579
- * Factory function to initialize shared state for callbacks.
580
- * Called once on cold start, result is cached and reused across invocations.
581
- * Always receives `bucket: BucketClient` (self-client). When deps/config
582
- * are declared, receives them as well.
583
- */
584
- setup?: SetupFactory$3<C, D, P, S>;
585
- /**
586
- * Dependencies on other handlers (tables, buckets, etc.).
587
- * Typed clients are injected into the handler via the `deps` argument.
588
- * Pass a function returning the deps object: `deps: () => ({ uploads })`.
589
- */
590
- deps?: () => D & {};
591
- /**
592
- * SSM Parameter Store parameters.
593
- * Declare with `param()` helper. Values are fetched and cached at cold start.
594
- */
595
- config?: P;
596
- /**
597
- * Static file glob patterns to bundle into the Lambda ZIP.
598
- * Files are accessible at runtime via the `files` callback argument.
599
- */
600
- static?: S;
601
- };
602
- /** With event handlers (at least one callback) */
603
- type DefineBucketWithHandlers<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {
604
- onObjectCreated?: BucketObjectCreatedFn<C, D, P, S>;
605
- onObjectRemoved?: BucketObjectRemovedFn<C, D, P, S>;
606
- };
607
- /** Resource-only: no Lambda, just creates the bucket */
608
- type DefineBucketResourceOnly<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {
609
- onObjectCreated?: never;
610
- onObjectRemoved?: never;
611
- };
612
- type DefineBucketOptions<C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined> = DefineBucketWithHandlers<C, D, P, S> | DefineBucketResourceOnly<C, D, P, S>;
412
+ } & SpreadCtx$3<C>) => Promise<void>;
613
413
  /**
614
414
  * Internal handler object created by defineBucket
615
415
  * @internal
@@ -618,7 +418,7 @@ type BucketHandler<C = any> = {
618
418
  readonly __brand: "effortless-bucket";
619
419
  readonly __spec: BucketConfig;
620
420
  readonly onError?: (...args: any[]) => any;
621
- readonly onAfterInvoke?: (...args: any[]) => any;
421
+ readonly onCleanup?: (...args: any[]) => any;
622
422
  readonly setup?: (...args: any[]) => C | Promise<C>;
623
423
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
624
424
  readonly config?: Record<string, unknown>;
@@ -626,42 +426,65 @@ type BucketHandler<C = any> = {
626
426
  readonly onObjectCreated?: (...args: any[]) => any;
627
427
  readonly onObjectRemoved?: (...args: any[]) => any;
628
428
  };
429
+ /** Options passed to `defineBucket()` — static config */
430
+ type BucketOptions = {
431
+ /** Lambda memory in MB (default: 256) */
432
+ memory?: number;
433
+ /** Lambda timeout (default: 30s) */
434
+ timeout?: Duration;
435
+ /** Additional IAM permissions for the Lambda */
436
+ permissions?: Permission[];
437
+ /** Logging verbosity */
438
+ logLevel?: LogLevel;
439
+ /** S3 key prefix filter for event notifications (e.g., "uploads/") */
440
+ prefix?: string;
441
+ /** S3 key suffix filter for event notifications (e.g., ".jpg") */
442
+ suffix?: string;
443
+ /** Static file glob patterns to bundle into the Lambda ZIP */
444
+ static?: string[];
445
+ };
446
+ interface BucketBuilder<D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
447
+ /** Declare handler dependencies */
448
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): BucketBuilder<D2, P, C, HasFiles>;
449
+ /** Declare SSM secrets */
450
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): BucketBuilder<D, P2, C, HasFiles>;
451
+ /** Initialize shared state on cold start. Receives bucket (self-client), deps, config, files. */
452
+ setup<C2>(fn: (args: SetupArgs$3<D, P, HasFiles>) => C2 | Promise<C2>): BucketBuilder<D, P, C2, HasFiles>;
453
+ /** Handle errors thrown by callbacks */
454
+ onError(fn: (args: {
455
+ error: unknown;
456
+ } & SpreadCtx$3<C>) => void): BucketBuilder<D, P, C, HasFiles>;
457
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
458
+ onCleanup(fn: (args: SpreadCtx$3<C>) => void | Promise<void>): BucketBuilder<D, P, C, HasFiles>;
459
+ /** Handle S3 ObjectCreated events (terminal — returns finalized handler) */
460
+ onObjectCreated(fn: BucketObjectCreatedFn<C>): BucketHandler<C>;
461
+ /** Handle S3 ObjectRemoved events (terminal — returns finalized handler) */
462
+ onObjectRemoved(fn: BucketObjectRemovedFn<C>): BucketHandler<C>;
463
+ /** Finalize as resource-only bucket (no Lambda) */
464
+ build(): BucketHandler<C>;
465
+ }
629
466
  /**
630
467
  * Define an S3 bucket with optional event handlers.
631
468
  *
632
- * Creates an S3 bucket. When event handlers are provided, also creates a Lambda
633
- * function triggered by S3 event notifications.
634
- *
635
469
  * @example Bucket with event handler
636
470
  * ```typescript
637
- * export const uploads = defineBucket({
638
- * prefix: "images/",
639
- * suffix: ".jpg",
640
- * onObjectCreated: async ({ event, bucket }) => {
641
- * const file = await bucket.get(event.key);
642
- * console.log("New upload:", event.key, file?.body.length);
643
- * }
644
- * });
645
- * ```
471
+ * export const uploads = defineBucket({ prefix: "images/", suffix: ".jpg" })
472
+ * .onObjectCreated(async ({ event, bucket }) => {
473
+ * console.log("New upload:", event.key);
474
+ * })
646
475
  *
647
- * @example Resource-only bucket (no Lambda)
648
- * ```typescript
649
- * export const assets = defineBucket({});
650
476
  * ```
651
477
  *
652
- * @example As a dependency
478
+ * @example Resource-only bucket (no Lambda)
653
479
  * ```typescript
654
- * export const api = defineApi({
655
- * basePath: "/process",
656
- * deps: { uploads },
657
- * post: async ({ req, deps }) => {
658
- * await deps.uploads.put("output.jpg", buffer);
659
- * return { status: 200, body: "OK" };
660
- * },
661
- * });
480
+ * export const assets = defineBucket().build()
662
481
  * ```
663
482
  */
664
- declare const defineBucket: <C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined>(options: DefineBucketOptions<C, D, P, S>) => BucketHandler<C>;
483
+ declare function defineBucket(): BucketBuilder;
484
+ declare function defineBucket(options: BucketOptions & {
485
+ static: string[];
486
+ }): BucketBuilder<undefined, undefined, undefined, true>;
487
+ declare function defineBucket(options: BucketOptions): BucketBuilder;
665
488
 
666
489
  /**
667
490
  * Configuration options for defining a mailer (SES email identity)
@@ -709,7 +532,7 @@ type MailerHandler = {
709
532
  * });
710
533
  * ```
711
534
  */
712
- declare const defineMailer: (options: MailerConfig) => MailerHandler;
535
+ declare const defineMailer: () => (options: MailerConfig) => MailerHandler;
713
536
 
714
537
  /**
715
538
  * Parsed SQS FIFO message passed to the handler callbacks.
@@ -746,7 +569,12 @@ type FifoQueueMessage<T = unknown> = {
746
569
  */
747
570
  type FifoQueueConfig = {
748
571
  /** Lambda function settings (memory, timeout, permissions, etc.) */
749
- lambda?: LambdaWithPermissions;
572
+ lambda?: {
573
+ memory?: number;
574
+ timeout?: Duration;
575
+ logLevel?: LogLevel;
576
+ permissions?: Permission[];
577
+ };
750
578
  /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */
751
579
  batchSize?: number;
752
580
  /** Maximum time to gather messages before invoking (default: 0). Accepts `"5s"`, `"1m"`, etc. */
@@ -762,81 +590,33 @@ type FifoQueueConfig = {
762
590
  /** Max number of receives before a message is sent to the dead-letter queue (default: 3) */
763
591
  maxReceiveCount?: number;
764
592
  };
765
- /**
766
- * Setup factory type always receives an args object.
767
- * Args include `deps` and/or `config` when declared (empty `{}` otherwise).
768
- */
769
- type SetupFactory$2<C, D, P, S extends string[] | undefined = undefined> = (args: ([D] extends [undefined] ? {} : {
593
+ /** Spread ctx into callback args (empty when no setup) */
594
+ type SpreadCtx$2<C> = [C] extends [undefined] ? {} : C & {};
595
+ /** Setup factory receives deps/config/files based on what was declared */
596
+ type SetupArgs$2<D, P, HasFiles extends boolean> = ([D] extends [undefined] ? {} : {
770
597
  deps: ResolveDeps<D>;
771
598
  }) & ([P] extends [undefined] ? {} : {
772
599
  config: ResolveConfig<P & {}>;
773
- }) & ([S] extends [undefined] ? {} : {
600
+ }) & (HasFiles extends true ? {
774
601
  files: StaticFiles;
775
- })) => C | Promise<C>;
602
+ } : {});
776
603
  /**
777
604
  * Per-message handler function.
778
605
  * Called once per message in the batch. Failures are reported individually.
779
606
  */
780
- type FifoQueueMessageFn<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
607
+ type FifoQueueMessageFn<T = unknown, C = undefined> = (args: {
781
608
  message: FifoQueueMessage<T>;
782
- } & HandlerArgs<C, D, P, S>) => Promise<void>;
609
+ } & SpreadCtx$2<C>) => Promise<void>;
783
610
  /**
784
611
  * Batch handler function.
785
612
  * Called once with all messages in the batch.
613
+ * Return `{ failures: string[] }` (messageIds) for partial batch failure reporting.
786
614
  */
787
- type FifoQueueBatchFn<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
615
+ type FifoQueueBatchFn<T = unknown, C = undefined> = (args: {
788
616
  messages: FifoQueueMessage<T>[];
789
- } & HandlerArgs<C, D, P, S>) => Promise<void>;
790
- /** Base options shared by all defineFifoQueue variants */
791
- type DefineFifoQueueBase<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = FifoQueueConfig & {
792
- /**
793
- * Decode/validate function for the message body.
794
- * Called with the JSON-parsed body; should return typed data or throw on validation failure.
795
- */
796
- schema?: (input: unknown) => T;
797
- /**
798
- * Error handler called when onMessage or onBatch throws.
799
- * If not provided, defaults to `console.error`.
800
- */
801
- onError?: (args: {
802
- error: unknown;
803
- } & HandlerArgs<C, D, P, S>) => void;
804
- /** Called after each invocation completes, right before Lambda freezes the process */
805
- onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<void>;
806
- /**
807
- * Factory function to initialize shared state for the handler.
808
- * Called once on cold start, result is cached and reused across invocations.
809
- * When deps/params are declared, receives them as argument.
810
- */
811
- setup?: SetupFactory$2<C, D, P, S>;
812
- /**
813
- * Dependencies on other handlers (tables, queues, etc.).
814
- * Typed clients are injected into the handler via the `deps` argument.
815
- * Pass a function returning the deps object: `deps: () => ({ orders })`.
816
- */
817
- deps?: () => D & {};
818
- /**
819
- * SSM Parameter Store parameters.
820
- * Declare with `param()` helper. Values are fetched and cached at cold start.
821
- */
822
- config?: P;
823
- /**
824
- * Static file glob patterns to bundle into the Lambda ZIP.
825
- * Files are accessible at runtime via the `files` callback argument.
826
- */
827
- static?: S;
828
- };
829
- /** Per-message processing */
830
- type DefineFifoQueueWithOnMessage<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {
831
- onMessage: FifoQueueMessageFn<T, C, D, P, S>;
832
- onBatch?: never;
833
- };
834
- /** Batch processing: all messages at once */
835
- type DefineFifoQueueWithOnBatch<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {
836
- onBatch: FifoQueueBatchFn<T, C, D, P, S>;
837
- onMessage?: never;
838
- };
839
- type DefineFifoQueueOptions<T = unknown, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueWithOnMessage<T, C, D, P, S> | DefineFifoQueueWithOnBatch<T, C, D, P, S>;
617
+ } & SpreadCtx$2<C>) => Promise<void | {
618
+ failures: string[];
619
+ }>;
840
620
  /**
841
621
  * Internal handler object created by defineFifoQueue
842
622
  * @internal
@@ -846,64 +626,211 @@ type FifoQueueHandler<T = unknown, C = any> = {
846
626
  readonly __spec: FifoQueueConfig;
847
627
  readonly schema?: (input: unknown) => T;
848
628
  readonly onError?: (...args: any[]) => any;
849
- readonly onAfterInvoke?: (...args: any[]) => any;
629
+ readonly onCleanup?: (...args: any[]) => any;
850
630
  readonly setup?: (...args: any[]) => C | Promise<C>;
851
631
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
852
632
  readonly config?: Record<string, unknown>;
853
633
  readonly static?: string[];
854
634
  readonly onMessage?: (...args: any[]) => any;
855
- readonly onBatch?: (...args: any[]) => any;
635
+ readonly onMessageBatch?: (...args: any[]) => any;
856
636
  };
637
+ /** Options passed to `defineFifoQueue()` — static config */
638
+ type FifoQueueOptions<T> = {
639
+ /** Lambda memory in MB (default: 256) */
640
+ memory?: number;
641
+ /** Lambda timeout (default: 30s) */
642
+ timeout?: Duration;
643
+ /** Additional IAM permissions for the Lambda */
644
+ permissions?: Permission[];
645
+ /** Logging verbosity */
646
+ logLevel?: LogLevel;
647
+ /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */
648
+ batchSize?: number;
649
+ /** Maximum time to gather messages before invoking (default: 0) */
650
+ batchWindow?: Duration;
651
+ /** Visibility timeout (default: max of timeout or 30s) */
652
+ visibilityTimeout?: Duration;
653
+ /** Message retention period (default: "4d") */
654
+ retentionPeriod?: Duration;
655
+ /** Delivery delay for all messages in the queue (default: 0) */
656
+ delay?: Duration;
657
+ /** Enable content-based deduplication (default: true) */
658
+ contentBasedDeduplication?: boolean;
659
+ /** Max number of receives before DLQ (default: 3) */
660
+ maxReceiveCount?: number;
661
+ /** Decode/validate function for the message body */
662
+ schema?: (input: unknown) => T;
663
+ /** Static file glob patterns to bundle into the Lambda ZIP */
664
+ static?: string[];
665
+ };
666
+ interface FifoQueueBuilder<T = unknown, D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
667
+ /** Declare handler dependencies */
668
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): FifoQueueBuilder<T, D2, P, C, HasFiles>;
669
+ /** Declare SSM secrets */
670
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): FifoQueueBuilder<T, D, P2, C, HasFiles>;
671
+ /** Initialize shared state on cold start. Receives deps, config, files. */
672
+ setup<C2>(fn: (args: SetupArgs$2<D, P, HasFiles>) => C2 | Promise<C2>): FifoQueueBuilder<T, D, P, C2, HasFiles>;
673
+ /** Handle errors thrown by message handlers */
674
+ onError(fn: (args: {
675
+ error: unknown;
676
+ } & SpreadCtx$2<C>) => void): FifoQueueBuilder<T, D, P, C, HasFiles>;
677
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
678
+ onCleanup(fn: (args: SpreadCtx$2<C>) => void | Promise<void>): FifoQueueBuilder<T, D, P, C, HasFiles>;
679
+ /** Per-message handler (terminal — returns finalized handler) */
680
+ onMessage(fn: FifoQueueMessageFn<T, C>): FifoQueueHandler<T, C>;
681
+ /** Batch handler (terminal — returns finalized handler) */
682
+ onMessageBatch(fn: FifoQueueBatchFn<T, C>): FifoQueueHandler<T, C>;
683
+ }
857
684
  /**
858
- * Define a FIFO SQS queue with a Lambda message handler
859
- *
860
- * Creates:
861
- * - SQS FIFO queue (with `.fifo` suffix)
862
- * - Lambda function triggered by the queue
863
- * - Event source mapping with partial batch failure support
685
+ * Define a FIFO SQS queue with a Lambda message handler.
864
686
  *
865
687
  * @example Per-message processing
866
688
  * ```typescript
867
- * type OrderEvent = { orderId: string; action: string };
868
- *
869
- * export const orderQueue = defineFifoQueue<OrderEvent>({
870
- * onMessage: async ({ message }) => {
689
+ * export const orderQueue = defineFifoQueue<OrderEvent>()
690
+ * .onMessage(async ({ message }) => {
871
691
  * console.log("Processing order:", message.body.orderId);
872
- * }
873
- * });
692
+ * })
693
+ *
874
694
  * ```
875
695
  *
876
696
  * @example Batch processing with schema
877
697
  * ```typescript
878
- * export const notifications = defineFifoQueue({
879
- * schema: (input) => NotificationSchema.parse(input),
880
- * batchSize: 5,
881
- * onBatch: async ({ messages }) => {
698
+ * export const notifications = defineFifoQueue({ batchSize: 5, schema: (i) => NotifSchema.parse(i) })
699
+ * .onMessageBatch(async ({ messages }) => {
882
700
  * await sendAll(messages.map(m => m.body));
883
- * }
884
- * });
701
+ * })
702
+ *
885
703
  * ```
886
704
  */
887
- declare const defineFifoQueue: <T = unknown, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined>(options: DefineFifoQueueOptions<T, C, D, P, S>) => FifoQueueHandler<T, C>;
705
+ declare function defineFifoQueue<T = unknown>(): FifoQueueBuilder<T>;
706
+ declare function defineFifoQueue<T = unknown>(options: FifoQueueOptions<T> & {
707
+ static: string[];
708
+ }): FifoQueueBuilder<T, undefined, undefined, undefined, true>;
709
+ declare function defineFifoQueue<T = unknown>(options: FifoQueueOptions<T>): FifoQueueBuilder<T>;
888
710
 
889
711
  /**
890
- * Options for sending an email via EmailClient.send()
712
+ * Sort key condition for TableClient.query()
891
713
  */
892
- type SendEmailBase = {
893
- /** Sender address (must be on a verified domain) */
894
- from: string;
895
- /** Recipient address(es) */
896
- to: string | string[];
897
- /** Email subject line */
898
- subject: string;
899
- };
900
- type SendEmailOptions = SendEmailBase & ({
901
- html: string;
902
- text?: string;
714
+ type SkCondition = string | {
715
+ begins_with: string;
903
716
  } | {
904
- html?: string;
905
- text: string;
906
- });
717
+ gt: string;
718
+ } | {
719
+ gte: string;
720
+ } | {
721
+ lt: string;
722
+ } | {
723
+ lte: string;
724
+ } | {
725
+ between: [string, string];
726
+ };
727
+ /**
728
+ * Query parameters for TableClient.query()
729
+ */
730
+ type QueryParams = {
731
+ /** Partition key value */
732
+ pk: string;
733
+ /** Optional sort key condition */
734
+ sk?: SkCondition;
735
+ /** Maximum number of items to return */
736
+ limit?: number;
737
+ /** Sort order (true = ascending, false = descending) */
738
+ scanIndexForward?: boolean;
739
+ };
740
+ /**
741
+ * Query parameters for TableClient.queryByTag() — cross-partition query via GSI.
742
+ * Uses the built-in `tag-pk-index` GSI (tag as partition key, pk as sort key).
743
+ */
744
+ type QueryByTagParams = {
745
+ /** Tag value (GSI partition key) — the entity type discriminant */
746
+ tag: string;
747
+ /** Optional pk condition (GSI sort key) */
748
+ pk?: SkCondition;
749
+ /** Maximum number of items to return */
750
+ limit?: number;
751
+ /** Sort order (true = ascending, false = descending) */
752
+ scanIndexForward?: boolean;
753
+ };
754
+ /** Extract keys of T whose values are arrays */
755
+ type ArrayKeys<T> = {
756
+ [K in keyof T]: T[K] extends unknown[] ? K : never;
757
+ }[keyof T];
758
+ /** Extract keys of T whose values are numbers */
759
+ type NumberKeys<T> = {
760
+ [K in keyof T]: T[K] extends number ? K : never;
761
+ }[keyof T];
762
+ /**
763
+ * Update actions for TableClient.update()
764
+ *
765
+ * `set`, `append`, and `remove` target fields inside the `data` attribute.
766
+ * Effortless auto-prefixes `data.` in the DynamoDB expression.
767
+ *
768
+ * @typeParam T - Type of the domain data (the `data` attribute)
769
+ */
770
+ type UpdateActions<T> = {
771
+ /** Set domain data fields (inside `data` attribute) */
772
+ set?: Partial<T>;
773
+ /** Atomically increment/decrement numeric fields inside `data` (use negative values to decrement) */
774
+ increment?: Pick<Partial<T>, NumberKeys<T>>;
775
+ /** Append elements to list fields inside `data` (creates the list if it doesn't exist) */
776
+ append?: Pick<Partial<T>, ArrayKeys<T>>;
777
+ /** Remove fields from `data` */
778
+ remove?: (keyof T)[];
779
+ /** Update the top-level `tag` attribute */
780
+ tag?: string;
781
+ /** Update TTL (set number or null to remove) */
782
+ ttl?: number | null;
783
+ };
784
+ /**
785
+ * Typed DynamoDB table client for single-table design.
786
+ *
787
+ * All items follow the `{ pk, sk, tag, data, ttl? }` structure.
788
+ * `T` is the domain data type stored in the `data` attribute.
789
+ *
790
+ * @typeParam T - Type of the domain data
791
+ */
792
+ /**
793
+ * Options for `put()` operation.
794
+ */
795
+ type PutOptions = {
796
+ /** When true, the put fails if an item with the same pk+sk already exists. */
797
+ ifNotExists?: boolean;
798
+ };
799
+ type TableClient<T = Record<string, unknown>> = {
800
+ /** Put an item. Tag is auto-extracted from `data[tagField]`. Use `ifNotExists` to prevent overwrites. */
801
+ put(item: PutInput<T>, options?: PutOptions): Promise<void>;
802
+ /** Get an item by pk + sk */
803
+ get(key: TableKey): Promise<TableItem<T> | undefined>;
804
+ /** Delete an item by pk + sk */
805
+ delete(key: TableKey): Promise<void>;
806
+ /** Update domain data fields without reading the full item */
807
+ update(key: TableKey, actions: UpdateActions<T>): Promise<void>;
808
+ /** Query by partition key with optional sort key condition */
809
+ query(params: QueryParams): Promise<TableItem<T>[]>;
810
+ /** Query by tag across all partitions via GSI (tag-pk-index). */
811
+ queryByTag(params: QueryByTagParams): Promise<TableItem<T>[]>;
812
+ /** The underlying DynamoDB table name */
813
+ tableName: string;
814
+ };
815
+
816
+ /**
817
+ * Options for sending an email via EmailClient.send()
818
+ */
819
+ type SendEmailBase = {
820
+ /** Sender address (must be on a verified domain) */
821
+ from: string;
822
+ /** Recipient address(es) */
823
+ to: string | string[];
824
+ /** Email subject line */
825
+ subject: string;
826
+ };
827
+ type SendEmailOptions = SendEmailBase & ({
828
+ html: string;
829
+ text?: string;
830
+ } | {
831
+ html?: string;
832
+ text: string;
833
+ });
907
834
  /**
908
835
  * Typed SES email client injected via deps.
909
836
  *
@@ -962,7 +889,12 @@ type StreamView = "NEW_AND_OLD_IMAGES" | "NEW_IMAGE" | "OLD_IMAGE" | "KEYS_ONLY"
962
889
  */
963
890
  type TableConfig = {
964
891
  /** Lambda function settings (memory, timeout, permissions, etc.) */
965
- lambda?: LambdaWithPermissions;
892
+ lambda?: {
893
+ memory?: number;
894
+ timeout?: Duration;
895
+ logLevel?: LogLevel;
896
+ permissions?: Permission[];
897
+ };
966
898
  /** DynamoDB billing mode (default: "PAY_PER_REQUEST") */
967
899
  billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
968
900
  /** Stream view type - what data to include in stream records (default: "NEW_AND_OLD_IMAGES") */
@@ -973,18 +905,12 @@ type TableConfig = {
973
905
  batchWindow?: Duration;
974
906
  /** Where to start reading the stream (default: "LATEST") */
975
907
  startingPosition?: "LATEST" | "TRIM_HORIZON";
908
+ /** Number of records to process concurrently within a batch (default: 1 — sequential) */
909
+ concurrency?: number;
976
910
  /**
977
911
  * Name of the field in `data` that serves as the entity type discriminant.
978
912
  * Effortless auto-copies `data[tagField]` to the top-level DynamoDB `tag` attribute on `put()`.
979
913
  * Defaults to `"tag"`.
980
- *
981
- * @example
982
- * ```typescript
983
- * export const orders = defineTable<{ type: "order"; amount: number }>({
984
- * tagField: "type",
985
- * onRecord: async ({ record }) => { ... }
986
- * });
987
- * ```
988
914
  */
989
915
  tagField?: string;
990
916
  };
@@ -1012,118 +938,40 @@ type TableRecord<T = Record<string, unknown>> = {
1012
938
  /** Approximate timestamp when the modification occurred */
1013
939
  timestamp?: number;
1014
940
  };
1015
- /**
1016
- * Information about a failed record during batch processing
1017
- *
1018
- * @typeParam T - Type of the domain data
1019
- */
1020
- type FailedRecord<T = Record<string, unknown>> = {
1021
- /** The record that failed to process */
1022
- record: TableRecord<T>;
1023
- /** The error that occurred */
1024
- error: unknown;
1025
- };
1026
- /**
1027
- * Setup factory type for table handlers.
1028
- * Always receives `table: TableClient<T>` (self-client for the handler's own table).
1029
- * Also receives `deps` and/or `config` when declared.
1030
- */
1031
- type SetupFactory$1<C, T, D, P, S extends string[] | undefined = undefined> = (args: {
941
+ /** Setup factory — receives table/deps/config/files based on what was declared */
942
+ type SetupArgs$1<T, D, P, HasFiles extends boolean> = {
1032
943
  table: TableClient<T>;
1033
944
  } & ([D] extends [undefined] ? {} : {
1034
945
  deps: ResolveDeps<D>;
1035
946
  }) & ([P] extends [undefined] ? {} : {
1036
947
  config: ResolveConfig<P & {}>;
1037
- }) & ([S] extends [undefined] ? {} : {
948
+ }) & (HasFiles extends true ? {
1038
949
  files: StaticFiles;
1039
- })) => C | Promise<C>;
950
+ } : {});
951
+ /** Spread ctx into callback args (empty when no setup) */
952
+ type SpreadCtx$1<C> = [C] extends [undefined] ? {} : C & {};
1040
953
  /**
1041
- * Callback function type for processing a single DynamoDB stream record
954
+ * Callback function type for processing a single DynamoDB stream record.
955
+ * Receives the current record and the full batch for context.
1042
956
  */
1043
- type TableRecordFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
957
+ type TableRecordFn<T = Record<string, unknown>, C = undefined> = (args: {
1044
958
  record: TableRecord<T>;
1045
- table: TableClient<T>;
1046
- } & HandlerArgs<C, D, P, S>) => Promise<R>;
1047
- /**
1048
- * Callback function type for processing accumulated batch results
1049
- */
1050
- type TableBatchCompleteFn<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
1051
- results: R[];
1052
- failures: FailedRecord<T>[];
1053
- table: TableClient<T>;
1054
- } & HandlerArgs<C, D, P, S>) => Promise<void>;
1055
- /**
1056
- * Callback function type for processing all records in a batch at once
1057
- */
1058
- type TableBatchFn<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = (args: {
1059
- records: TableRecord<T>[];
1060
- table: TableClient<T>;
1061
- } & HandlerArgs<C, D, P, S>) => Promise<void>;
1062
- /** Base options shared by all defineTable variants */
1063
- type DefineTableBase<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = Omit<TableConfig, "tagField"> & {
1064
- /** Name of the field in `data` that serves as the entity type discriminant (default: `"tag"`). */
1065
- tagField?: Extract<keyof T, string>;
1066
- /**
1067
- * Decode/validate function for the `data` portion of stream record items.
1068
- * Called with the unmarshalled `data` attribute; should return typed data or throw on validation failure.
1069
- * When provided, T is inferred from the return type — no need to specify generic parameters.
1070
- */
1071
- schema?: (input: unknown) => T;
1072
- /**
1073
- * Error handler called when onRecord, onBatch, or onBatchComplete throws.
1074
- * Receives the error. If not provided, defaults to `console.error`.
1075
- */
1076
- onError?: (args: {
1077
- error: unknown;
1078
- } & HandlerArgs<C, D, P, S>) => void;
1079
- /** Called after each invocation completes, right before Lambda freezes the process */
1080
- onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<void>;
1081
- /**
1082
- * Factory function to initialize shared state for callbacks.
1083
- * Called once on cold start, result is cached and reused across invocations.
1084
- * When deps/params are declared, receives them as argument.
1085
- * Supports both sync and async return values.
1086
- */
1087
- setup?: SetupFactory$1<C, T, D, P, S>;
1088
- /**
1089
- * Dependencies on other handlers (tables, queues, etc.).
1090
- * Typed clients are injected into the handler via the `deps` argument.
1091
- * Pass a function returning the deps object: `deps: () => ({ orders })`.
1092
- */
1093
- deps?: () => D & {};
1094
- /**
1095
- * SSM Parameter Store parameters.
1096
- * Declare with `param()` helper. Values are fetched and cached at cold start.
1097
- * Typed values are injected into the handler via the `config` argument.
1098
- */
1099
- config?: P;
1100
- /**
1101
- * Static file glob patterns to bundle into the Lambda ZIP.
1102
- * Files are accessible at runtime via the `files` callback argument.
1103
- */
1104
- static?: S;
1105
- };
1106
- /** Per-record processing: onRecord with optional onBatchComplete */
1107
- type DefineTableWithOnRecord<T = Record<string, unknown>, C = undefined, R = void, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {
1108
- onRecord: TableRecordFn<T, C, R, D, P, S>;
1109
- onBatchComplete?: TableBatchCompleteFn<T, C, R, D, P, S>;
1110
- onBatch?: never;
1111
- };
1112
- /** Batch processing: onBatch processes all records at once */
1113
- type DefineTableWithOnBatch<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {
1114
- onBatch: TableBatchFn<T, C, D, P, S>;
1115
- onRecord?: never;
1116
- onBatchComplete?: never;
1117
- };
1118
- /** Resource-only: no handler, just creates the table */
1119
- type DefineTableResourceOnly<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & {
1120
- onRecord?: never;
1121
- onBatch?: never;
1122
- onBatchComplete?: never;
1123
- };
1124
- type DefineTableOptions<T = Record<string, unknown>, C = undefined, R = void, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined> = DefineTableWithOnRecord<T, C, R, D, P, S> | DefineTableWithOnBatch<T, C, D, P, S> | DefineTableResourceOnly<T, C, D, P, S>;
959
+ batch: readonly TableRecord<T>[];
960
+ } & SpreadCtx$1<C>) => Promise<void>;
961
+ /**
962
+ * Batch handler function for DynamoDB stream records.
963
+ * Called once with all records in the batch.
964
+ * Return `{ failures: string[] }` (sequence numbers) for partial batch failure reporting.
965
+ */
966
+ type TableBatchFn<T = Record<string, unknown>, C = undefined> = (args: {
967
+ records: readonly TableRecord<T>[];
968
+ } & SpreadCtx$1<C>) => Promise<void | {
969
+ failures: string[];
970
+ }>;
971
+ /** Static config extracted by AST (no runtime callbacks) */
1125
972
  /**
1126
- * Internal handler object created by defineTable
973
+ * Handler object created by defineTable.
974
+ * Used by runtime wrappers and as type annotation for circular deps.
1127
975
  * @internal
1128
976
  */
1129
977
  type TableHandler<T = Record<string, unknown>, C = any> = {
@@ -1131,15 +979,63 @@ type TableHandler<T = Record<string, unknown>, C = any> = {
1131
979
  readonly __spec: TableConfig;
1132
980
  readonly schema?: (input: unknown) => T;
1133
981
  readonly onError?: (...args: any[]) => any;
1134
- readonly onAfterInvoke?: (...args: any[]) => any;
982
+ readonly onCleanup?: (...args: any[]) => any;
1135
983
  readonly setup?: (...args: any[]) => C | Promise<C>;
1136
984
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1137
985
  readonly config?: Record<string, unknown>;
1138
986
  readonly static?: string[];
1139
987
  readonly onRecord?: (...args: any[]) => any;
1140
- readonly onBatchComplete?: (...args: any[]) => any;
1141
- readonly onBatch?: (...args: any[]) => any;
988
+ readonly onRecordBatch?: (...args: any[]) => any;
1142
989
  };
990
+ /** Options passed to `defineTable()` — static config, no generics needed for inference */
991
+ type TableOptions<T> = {
992
+ /** Lambda memory in MB (default: 256) */
993
+ memory?: number;
994
+ /** Lambda timeout (default: 30s). Accepts seconds or duration string: `"30s"`, `"5m"` */
995
+ timeout?: Duration;
996
+ /** Additional IAM permissions for the Lambda */
997
+ permissions?: Permission[];
998
+ /** Logging verbosity */
999
+ logLevel?: LogLevel;
1000
+ /** DynamoDB billing mode (default: "PAY_PER_REQUEST") */
1001
+ billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
1002
+ /** Stream view type (default: "NEW_AND_OLD_IMAGES") */
1003
+ streamView?: StreamView;
1004
+ /** Number of records to process in each Lambda invocation (1-10000, default: 100) */
1005
+ batchSize?: number;
1006
+ /** Maximum time to gather records before invoking (default: "2s") */
1007
+ batchWindow?: Duration;
1008
+ /** Where to start reading the stream (default: "LATEST") */
1009
+ startingPosition?: "LATEST" | "TRIM_HORIZON";
1010
+ /** Number of records to process concurrently within a batch (default: 1) */
1011
+ concurrency?: number;
1012
+ /** Name of the field in `data` that serves as the entity type discriminant (default: "tag") */
1013
+ tagField?: Extract<keyof T, string>;
1014
+ /** Decode/validate function for the `data` portion of stream records */
1015
+ schema?: (input: unknown) => T;
1016
+ /** Static file glob patterns to bundle into the Lambda ZIP */
1017
+ static?: string[];
1018
+ };
1019
+ interface TableBuilder<T = Record<string, unknown>, D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
1020
+ /** Declare handler dependencies (tables, queues, buckets, mailers) */
1021
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): TableBuilder<T, D2, P, C, HasFiles>;
1022
+ /** Declare SSM secrets */
1023
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): TableBuilder<T, D, P2, C, HasFiles>;
1024
+ /** Initialize shared state on cold start. Receives table (self-client), deps, config, files. */
1025
+ setup<C2>(fn: (args: SetupArgs$1<T, D, P, HasFiles>) => C2 | Promise<C2>): TableBuilder<T, D, P, C2, HasFiles>;
1026
+ /** Handle errors thrown by onRecord/onRecordBatch */
1027
+ onError(fn: (args: {
1028
+ error: unknown;
1029
+ } & SpreadCtx$1<C>) => void): TableBuilder<T, D, P, C, HasFiles>;
1030
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
1031
+ onCleanup(fn: (args: SpreadCtx$1<C>) => void | Promise<void>): TableBuilder<T, D, P, C, HasFiles>;
1032
+ /** Per-record stream handler (terminal — returns finalized handler) */
1033
+ onRecord(fn: TableRecordFn<T, C>): TableHandler<T, C>;
1034
+ /** Batch stream handler (terminal — returns finalized handler) */
1035
+ onRecordBatch(fn: TableBatchFn<T, C>): TableHandler<T, C>;
1036
+ /** Finalize as resource-only table (no Lambda) */
1037
+ build(): TableHandler<T, C>;
1038
+ }
1143
1039
  /**
1144
1040
  * Define a DynamoDB table with optional stream handler (single-table design).
1145
1041
  *
@@ -1148,25 +1044,30 @@ type TableHandler<T = Record<string, unknown>, C = any> = {
1148
1044
  *
1149
1045
  * @example Table with stream handler
1150
1046
  * ```typescript
1151
- * type OrderData = { amount: number; status: string };
1152
- *
1153
- * export const orders = defineTable<OrderData>({
1154
- * streamView: "NEW_AND_OLD_IMAGES",
1155
- * batchSize: 10,
1156
- * onRecord: async ({ record }) => {
1047
+ * export const orders = defineTable<OrderData>({ batchSize: 10, concurrency: 5 })
1048
+ * .setup(({ table }) => ({ table }))
1049
+ * .onRecord(async ({ record, table }) => {
1157
1050
  * if (record.eventName === "INSERT") {
1158
- * console.log("New order:", record.new?.data.amount);
1051
+ * console.log("New order:", record.new?.data);
1159
1052
  * }
1160
- * }
1161
- * });
1053
+ * })
1162
1054
  * ```
1163
1055
  *
1164
1056
  * @example Table only (no Lambda)
1165
1057
  * ```typescript
1166
- * export const users = defineTable({});
1058
+ * export const users = defineTable<User>().build()
1059
+ * ```
1060
+ *
1061
+ * @example Table as dependency (resource-only, no Lambda)
1062
+ * ```typescript
1063
+ * export const sessions = defineTable<Session>().build()
1167
1064
  * ```
1168
1065
  */
1169
- declare const defineTable: <T = Record<string, unknown>, C = undefined, R = void, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined>(options: DefineTableOptions<T, C, R, D, P, S>) => TableHandler<T, C>;
1066
+ declare function defineTable<T = Record<string, unknown>>(): TableBuilder<T>;
1067
+ declare function defineTable<T = Record<string, unknown>>(options: TableOptions<T> & {
1068
+ static: string[];
1069
+ }): TableBuilder<T, undefined, undefined, undefined, true>;
1070
+ declare function defineTable<T = Record<string, unknown>>(options: TableOptions<T>): TableBuilder<T>;
1170
1071
 
1171
1072
  /**
1172
1073
  * Configuration for deploying an SSR framework (Nuxt, Astro, etc.)
@@ -1223,94 +1124,7 @@ type AppHandler = {
1223
1124
  * });
1224
1125
  * ```
1225
1126
  */
1226
- declare const defineApp: (options: AppConfig) => AppHandler;
1227
-
1228
- type AuthConfig<_T = undefined> = {
1229
- /** Path to redirect unauthenticated users to (used by static sites). */
1230
- loginPath: string;
1231
- /** Paths that don't require authentication. Supports trailing `*` wildcard. */
1232
- public?: string[];
1233
- /** Default session lifetime (default: "7d"). Accepts seconds or duration string. */
1234
- expiresIn?: Duration;
1235
- };
1236
- /**
1237
- * Branded auth object returned by `defineAuth()`.
1238
- * Pass to `defineApi({ auth })` and `defineStaticSite({ auth })`.
1239
- */
1240
- type Auth<T = undefined> = AuthConfig<T> & {
1241
- readonly __brand: "effortless-auth";
1242
- /** @internal phantom type marker for session data */
1243
- readonly __session?: T;
1244
- };
1245
- /** API token authentication strategy. Verifies tokens from HTTP headers (e.g. Authorization: Bearer). */
1246
- type ApiTokenStrategy<T, D = undefined> = {
1247
- /** HTTP header to read the token from. Default: "authorization" (strips "Bearer " prefix). */
1248
- header?: string;
1249
- /** Verify the token value and return session data, or null if invalid. */
1250
- verify: [D] extends [undefined] ? (value: string) => T | null | Promise<T | null> : (value: string, ctx: {
1251
- deps: D;
1252
- }) => T | null | Promise<T | null>;
1253
- /** Cache verified token results for this duration. Avoids calling verify on every request. */
1254
- cacheTtl?: Duration;
1255
- };
1256
- /**
1257
- * Define authentication for API handlers and static sites.
1258
- *
1259
- * Session-based auth uses HMAC-signed cookies (auto-managed by the framework).
1260
- *
1261
- * - Lambda@Edge middleware verifies cookie signatures for static sites
1262
- * - API handler gets `auth.createSession()` / `auth.clearSession()` / `auth.session` helpers
1263
- * - HMAC secret is auto-generated and stored in SSM Parameter Store
1264
- *
1265
- * @typeParam T - Session data type. When provided, `createSession(data)` requires typed payload
1266
- * and `auth.session` is typed as `T` in handler args.
1267
- *
1268
- * @example
1269
- * ```typescript
1270
- * type Session = { userId: string; role: "admin" | "user" };
1271
- *
1272
- * const auth = defineAuth<Session>({
1273
- * loginPath: '/login',
1274
- * public: ['/login', '/api/login'],
1275
- * expiresIn: '7d',
1276
- * })
1277
- *
1278
- * export const api = defineApi({ auth, ... })
1279
- * export const webapp = defineStaticSite({ auth, ... })
1280
- * ```
1281
- */
1282
- declare const defineAuth: <T = undefined>(options: AuthConfig<T>) => Auth<T>;
1283
- /** Options for creating a session */
1284
- type SessionOptions = {
1285
- expiresIn?: Duration;
1286
- };
1287
- /** Session response with Set-Cookie header */
1288
- type SessionResponse = {
1289
- status: 200;
1290
- body: {
1291
- ok: true;
1292
- };
1293
- headers: Record<string, string>;
1294
- };
1295
- /**
1296
- * Auth helpers injected into API handler callback args when `auth` is configured.
1297
- * @typeParam T - Session data type (undefined = no custom data)
1298
- */
1299
- type AuthHelpers<T = undefined> = {
1300
- clearSession(): {
1301
- status: 200;
1302
- body: {
1303
- ok: true;
1304
- };
1305
- headers: Record<string, string>;
1306
- };
1307
- /** The current session data (from cookie or API token). Undefined if no valid session. */
1308
- session: T extends undefined ? undefined : T | undefined;
1309
- } & ([T] extends [undefined] ? {
1310
- createSession(options?: SessionOptions): SessionResponse;
1311
- } : {
1312
- createSession(data: T, options?: SessionOptions): SessionResponse;
1313
- });
1127
+ declare const defineApp: () => (options: AppConfig) => AppHandler;
1314
1128
 
1315
1129
  /** Any branded handler that deploys to API Gateway (HttpHandler, ApiHandler, etc.) */
1316
1130
  type AnyRoutableHandler = {
@@ -1371,8 +1185,6 @@ type StaticSiteConfig = {
1371
1185
  errorPage?: string;
1372
1186
  /** Lambda@Edge middleware that runs before serving pages. Use for auth checks, redirects, etc. */
1373
1187
  middleware?: MiddlewareHandler;
1374
- /** Cookie-based authentication. Auto-generates Lambda@Edge middleware that verifies signed cookies. */
1375
- auth?: Auth<any>;
1376
1188
  /** SEO: auto-generate sitemap.xml and robots.txt at deploy time, optionally submit URLs to Google Indexing API */
1377
1189
  seo?: StaticSiteSeo;
1378
1190
  };
@@ -1407,48 +1219,115 @@ type StaticSiteHandler = {
1407
1219
  * });
1408
1220
  * ```
1409
1221
  *
1410
- * @example Protected site with middleware (Lambda@Edge)
1411
- * ```typescript
1412
- * export const admin = defineStaticSite({
1413
- * dir: "admin/dist",
1414
- * middleware: async (request) => {
1415
- * if (!request.cookies.session) {
1416
- * return { redirect: "/login" };
1417
- * }
1418
- * },
1419
- * });
1420
- * ```
1421
1222
  */
1422
- declare const defineStaticSite: (options: StaticSiteConfig) => StaticSiteHandler;
1223
+ declare const defineStaticSite: () => (options: StaticSiteConfig) => StaticSiteHandler;
1423
1224
 
1424
- /** Extract session type T from Auth<T> */
1425
- type SessionOf<A> = A extends Auth<infer T> ? T : undefined;
1426
- /** GET route handler — no schema, no data */
1427
- type ApiGetHandlerFn<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends Auth<any> | undefined = undefined> = (args: {
1428
- req: HttpRequest;
1429
- } & HandlerArgs<C, D, P, S> & ([ST] extends [true] ? {
1430
- stream: ResponseStream;
1431
- } : {}) & ([A] extends [undefined] ? {} : {
1432
- auth: AuthHelpers<SessionOf<A>>;
1433
- })) => Promise<HttpResponse | void> | HttpResponse | void;
1434
- /** POST handler — with typed data from schema */
1435
- type ApiPostHandlerFn<T = undefined, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends Auth<any> | undefined = undefined> = (args: {
1225
+ /** Options for creating a session */
1226
+ type SessionOptions = {
1227
+ expiresIn?: Duration;
1228
+ };
1229
+ /** Session response with Set-Cookie header */
1230
+ type SessionResponse = {
1231
+ status: 200;
1232
+ body: {
1233
+ ok: true;
1234
+ };
1235
+ headers: Record<string, string>;
1236
+ };
1237
+ /**
1238
+ * Auth helpers injected into API handler callback args when `auth` is configured.
1239
+ * @typeParam T - Session data type (from `AuthOptions<T>`)
1240
+ */
1241
+ type AuthHelpers<T = unknown> = {
1242
+ /** Create a signed session cookie with typed data. */
1243
+ createSession(data: T, options?: SessionOptions): SessionResponse;
1244
+ /** Clear the session cookie. */
1245
+ clearSession(): {
1246
+ status: 200;
1247
+ body: {
1248
+ ok: true;
1249
+ };
1250
+ headers: Record<string, string>;
1251
+ };
1252
+ /** The current session data (from cookie or API token). Undefined if no valid session. */
1253
+ session: T | undefined;
1254
+ };
1255
+
1256
+ /** Auth config options (user-facing) */
1257
+ type AuthOptions<A = unknown> = {
1258
+ /** HMAC secret for signing session cookies. Use `secret()` or `param()` in config. */
1259
+ secret: string;
1260
+ /** Default session lifetime (default: "7d"). */
1261
+ expiresIn?: Duration;
1262
+ /** Optional API token strategy for external clients. */
1263
+ apiToken?: {
1264
+ /** HTTP header to read the token from. Default: "authorization" (strips "Bearer " prefix). */
1265
+ header?: string;
1266
+ /** Verify the token value and return session data, or null if invalid. */
1267
+ verify: (value: string) => A | null | Promise<A | null>;
1268
+ /** Cache verified token results for this duration. */
1269
+ cacheTtl?: Duration;
1270
+ };
1271
+ };
1272
+ /** Branded auth config — created by `enableAuth<A>()` helper, carries session type A */
1273
+ type ApiAuthConfig<A = unknown> = AuthOptions<A> & {
1274
+ readonly __sessionType: A;
1275
+ };
1276
+ /** Type of the `enableAuth` helper injected into setup args */
1277
+ type EnableAuth = <A = unknown>(options: AuthOptions<A>) => ApiAuthConfig<A>;
1278
+ /** Extract session type A from ctx.auth if present */
1279
+ type ExtractAuth<C> = C extends {
1280
+ auth: ApiAuthConfig<infer A>;
1281
+ } ? A : undefined;
1282
+ /** Property names reserved by the framework — cannot be used in setup return */
1283
+ type ReservedKeys = 'req' | 'input' | 'stream' | 'ok' | 'fail';
1284
+ /** Success response helper: `ok({ data })` → `{ status: 200, body: { data } }` */
1285
+ type OkHelper = (body?: unknown, status?: number) => HttpResponse;
1286
+ /** Error response helper: `fail("message")` → `{ status: 400, body: { error: "message" } }` */
1287
+ type FailHelper = (message: string, status?: number) => HttpResponse;
1288
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
1289
+ /** Parsed route definition stored at runtime */
1290
+ type RouteEntry = {
1291
+ method: HttpMethod;
1292
+ path: string;
1293
+ onRequest: (...args: any[]) => any;
1294
+ public?: boolean;
1295
+ };
1296
+ /** Spread ctx into route args: Omit auth config, add AuthHelpers if present */
1297
+ type SpreadCtx<C> = ([C] extends [undefined] ? {} : Omit<C & {}, 'auth'>) & ([ExtractAuth<C>] extends [undefined] ? {} : {
1298
+ auth: AuthHelpers<ExtractAuth<C>>;
1299
+ });
1300
+ /** Callback args available inside each route — ctx is spread into args */
1301
+ type RouteArgs<C, ST> = SpreadCtx<C> & {
1436
1302
  req: HttpRequest;
1437
- } & ([T] extends [undefined] ? {} : {
1438
- data: T;
1439
- }) & HandlerArgs<C, D, P, S> & ([ST] extends [true] ? {
1303
+ input: unknown;
1304
+ ok: OkHelper;
1305
+ fail: FailHelper;
1306
+ } & ([ST] extends [true] ? {
1440
1307
  stream: ResponseStream;
1441
- } : {}) & ([A] extends [undefined] ? {} : {
1442
- auth: AuthHelpers<SessionOf<A>>;
1443
- })) => Promise<HttpResponse | void> | HttpResponse | void;
1444
- /** Setup factory receives deps/config/files when declared */
1445
- type SetupFactory<C, D, P, S extends string[] | undefined = undefined> = (args: ([D] extends [undefined] ? {} : {
1308
+ } : {});
1309
+ /** Route handler function */
1310
+ type RouteHandler<C, ST> = (args: RouteArgs<C, ST>) => Promise<HttpResponse | void> | HttpResponse | void;
1311
+ /** Route options (e.g. public) */
1312
+ type RouteOptions = {
1313
+ public?: boolean;
1314
+ };
1315
+ /** Setup factory — receives deps/config/files/enableAuth based on what was declared */
1316
+ type SetupArgs<D, P, HasFiles extends boolean> = {
1317
+ enableAuth: EnableAuth;
1318
+ ok: OkHelper;
1319
+ fail: FailHelper;
1320
+ } & ([D] extends [undefined] ? {} : {
1446
1321
  deps: ResolveDeps<D>;
1447
1322
  }) & ([P] extends [undefined] ? {} : {
1448
1323
  config: ResolveConfig<P & {}>;
1449
- }) & ([S] extends [undefined] ? {} : {
1324
+ }) & (HasFiles extends true ? {
1450
1325
  files: StaticFiles;
1451
- })) => C | Promise<C>;
1326
+ } : {});
1327
+ /** Validate that setup return type does not use reserved property names */
1328
+ type ValidateSetupReturn<C> = C & {
1329
+ [K in ReservedKeys]?: never;
1330
+ };
1452
1331
  /** Static config extracted by AST (no runtime callbacks) */
1453
1332
  type ApiConfig = {
1454
1333
  /** Lambda function settings (memory, timeout, permissions, etc.) */
@@ -1458,104 +1337,104 @@ type ApiConfig = {
1458
1337
  /** Enable response streaming. When true, the Lambda Function URL uses RESPONSE_STREAM invoke mode. */
1459
1338
  stream?: boolean;
1460
1339
  };
1461
- /**
1462
- * Options for defining a CQRS-style API endpoint.
1463
- *
1464
- * - `get` routes handle queries (path-based routing, no body)
1465
- * - `post` handles commands (single entry point, discriminated union via `schema`)
1466
- */
1467
- type DefineApiOptions<T = undefined, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends Auth<any> | undefined = undefined> = {
1468
- /** Lambda function settings (memory, timeout, permissions, etc.) */
1469
- lambda?: LambdaWithPermissions;
1470
- /** Base path prefix for all routes (e.g., "/api") */
1471
- basePath: `/${string}`;
1472
- /** Enable response streaming. When true, routes receive a `stream` arg for SSE. Routes can still return HttpResponse normally. */
1473
- stream?: ST;
1474
- /** Session-based authentication. Injects `auth` helpers (createSession/clearSession/session) into handler args. */
1475
- auth?: A;
1476
- /** API token authentication for external clients (Bearer tokens, API keys). Has access to deps. */
1477
- apiToken?: ApiTokenStrategy<SessionOf<A>, [D] extends [undefined] ? undefined : ResolveDeps<D>>;
1478
- /**
1479
- * Factory function to initialize shared state.
1480
- * Called once on cold start, result is cached and reused across invocations.
1481
- */
1482
- setup?: SetupFactory<C, D, P, S>;
1483
- /** Dependencies on other handlers (tables, queues, etc.): `deps: () => ({ users })` */
1484
- deps?: () => D & {};
1485
- /** SSM Parameter Store parameters */
1486
- config?: P;
1487
- /** Static file glob patterns to bundle into the Lambda ZIP */
1488
- static?: S;
1489
- /** Error handler called when schema validation or handler throws */
1490
- onError?: (args: {
1491
- error: unknown;
1492
- req: HttpRequest;
1493
- } & HandlerArgs<C, D, P, S>) => HttpResponse;
1494
- /** Called after each invocation completes, right before Lambda freezes the process */
1495
- onAfterInvoke?: (args: HandlerArgs<C, D, P, S>) => void | Promise<void>;
1496
- /** GET routes — query handlers keyed by relative path (e.g., "/users/{id}") */
1497
- get?: Record<`/${string}`, ApiGetHandlerFn<C, D, P, S, ST, A>>;
1498
- /**
1499
- * Schema for POST body validation. Use with discriminated unions:
1500
- * ```typescript
1501
- * schema: Action.parse,
1502
- * post: async ({ data }) => { switch (data.action) { ... } }
1503
- * ```
1504
- */
1505
- schema?: (input: unknown) => T;
1506
- /** POST handler — single entry point for commands */
1507
- post?: ApiPostHandlerFn<T, C, D, P, S, ST, A>;
1508
- };
1509
1340
  /** Internal handler object created by defineApi */
1510
- type ApiHandler<T = undefined, C = undefined> = {
1341
+ type ApiHandler<C = undefined> = {
1511
1342
  readonly __brand: "effortless-api";
1512
1343
  readonly __spec: ApiConfig;
1513
- readonly schema?: (input: unknown) => T;
1514
1344
  readonly onError?: (...args: any[]) => any;
1515
- readonly onAfterInvoke?: (...args: any[]) => any;
1345
+ readonly onCleanup?: (...args: any[]) => any;
1516
1346
  readonly setup?: (...args: any[]) => C | Promise<C>;
1517
1347
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1518
1348
  readonly config?: Record<string, unknown>;
1519
1349
  readonly static?: string[];
1520
- readonly auth?: Auth;
1521
- readonly apiToken?: ApiTokenStrategy<any, any>;
1522
- readonly get?: Record<`/${string}`, (...args: any[]) => any>;
1523
- readonly post?: (...args: any[]) => any;
1350
+ readonly routes?: RouteEntry[];
1524
1351
  };
1525
- /**
1526
- * Define a CQRS-style API with typed GET routes and POST commands.
1527
- *
1528
- * GET routes handle queries — path-based routing, no request body.
1529
- * POST handles commands single entry point with discriminated union schema.
1530
- * Deploys as a single Lambda (fat Lambda) with one API Gateway catch-all route.
1352
+ /** Options passed to `defineApi()` */
1353
+ type ApiOptions = {
1354
+ /** Base path prefix for all routes (e.g., "/api") */
1355
+ basePath: `/${string}`;
1356
+ /** Lambda memory in MB (default: 256) */
1357
+ memory?: number;
1358
+ /** Lambda timeout (default: 30s). Accepts seconds or duration string: `"30s"`, `"5m"` */
1359
+ timeout?: Duration;
1360
+ /** Additional IAM permissions for the Lambda */
1361
+ permissions?: Permission[];
1362
+ /** Logging verbosity: "error" (errors only), "info" (+ execution summary), "debug" (+ input/output). Default: "info" */
1363
+ logLevel?: LogLevel;
1364
+ /** Enable response streaming. When true, routes receive a `stream` arg for SSE. */
1365
+ stream?: boolean;
1366
+ /** Static file glob patterns to bundle into the Lambda ZIP */
1367
+ static?: string[];
1368
+ };
1369
+ /**
1370
+ * Finalized API handler with route-adding methods.
1371
+ * Has `__brand` so CLI discovers it. Each `.get()/.post()` adds a route and returns self.
1372
+ */
1373
+ interface ApiRoutes<C = undefined, ST extends boolean = false> extends ApiHandler<C> {
1374
+ get(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1375
+ post(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1376
+ put(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1377
+ patch(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1378
+ delete(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1379
+ }
1380
+ /**
1381
+ * Builder interface for defining API handlers.
1382
+ *
1383
+ * Each method sets exactly one generic, so inference happens one step at a time.
1384
+ * This prevents cascading type errors when one property has a mistake.
1385
+ */
1386
+ interface ApiBuilder<D = undefined, P = undefined, C = undefined, ST extends boolean = false, HasFiles extends boolean = false> {
1387
+ /** Declare handler dependencies (tables, queues, buckets, mailers) */
1388
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): ApiBuilder<D2, P, C, ST, HasFiles>;
1389
+ /** Declare SSM secrets */
1390
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): ApiBuilder<D, P2, C, ST, HasFiles>;
1391
+ /** Initialize shared state on cold start. Receives deps/config/files based on what was declared. */
1392
+ setup<C2>(fn: (args: SetupArgs<D, P, HasFiles>) => ValidateSetupReturn<C2> | Promise<ValidateSetupReturn<C2>>): ApiBuilder<D, P, C2, ST, HasFiles>;
1393
+ /** Handle errors thrown by routes */
1394
+ onError(fn: (args: {
1395
+ error: unknown;
1396
+ req: HttpRequest;
1397
+ ok: OkHelper;
1398
+ fail: FailHelper;
1399
+ } & SpreadCtx<C>) => HttpResponse): ApiBuilder<D, P, C, ST, HasFiles>;
1400
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
1401
+ onCleanup(fn: (args: SpreadCtx<C>) => void | Promise<void>): ApiBuilder<D, P, C, ST, HasFiles>;
1402
+ /** Add a GET route (terminal — returns finalized handler with route methods) */
1403
+ get(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1404
+ /** Add a POST route (terminal) */
1405
+ post(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1406
+ /** Add a PUT route (terminal) */
1407
+ put(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1408
+ /** Add a PATCH route (terminal) */
1409
+ patch(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1410
+ /** Add a DELETE route (terminal) */
1411
+ delete(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1412
+ }
1413
+ /**
1414
+ * Define an API with typed routes using a builder pattern.
1531
1415
  *
1532
1416
  * @example
1533
1417
  * ```typescript
1534
- * export default defineApi({
1535
- * basePath: "/api",
1536
- * deps: { users },
1537
- *
1538
- * get: {
1539
- * "/users": async ({ req, deps }) => ({
1540
- * status: 200,
1541
- * body: await deps.users.scan()
1542
- * }),
1543
- * "/users/{id}": async ({ req, deps }) => ({
1544
- * status: 200,
1545
- * body: await deps.users.get(req.params.id)
1546
- * }),
1547
- * },
1548
- *
1549
- * schema: Action.parse,
1550
- * post: async ({ data, deps }) => {
1551
- * switch (data.action) {
1552
- * case "create": return { status: 201, body: await deps.users.put(data) }
1553
- * case "delete": return { status: 200, body: await deps.users.delete(data.id) }
1554
- * }
1555
- * },
1556
- * })
1418
+ * // Minimal
1419
+ * export default defineApi({ basePath: "/hello" })
1420
+ * .get("/", async ({ req, ok }) => ok({ message: "Hello!" }))
1421
+ *
1422
+ * // Full
1423
+ * export const api = defineApi({ basePath: "/api", timeout: "30s" })
1424
+ * .deps(() => ({ users }))
1425
+ * .config(({ defineSecret }) => ({ dbUrl: defineSecret() }))
1426
+ * .setup(async ({ deps, config, enableAuth }) => ({
1427
+ * users: deps.users,
1428
+ * auth: enableAuth<Session>({ secret: config.dbUrl }),
1429
+ * }))
1430
+ * .onError(({ error, fail }) => fail(String(error), 500))
1431
+ * .get("/me", async ({ users, auth, ok }) => ok(auth.session))
1432
+ * .post("/login", async ({ auth, ok }) => ok(await auth.createSession()), { public: true })
1557
1433
  * ```
1558
1434
  */
1559
- declare const defineApi: <T = undefined, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnyParamRef> | undefined = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined, A extends Auth<any> | undefined = undefined>(options: DefineApiOptions<T, C, D, P, S, ST, A>) => ApiHandler<T, C>;
1435
+ declare function defineApi(options: ApiOptions & {
1436
+ static: string[];
1437
+ }): ApiBuilder<undefined, undefined, undefined, false, true>;
1438
+ declare function defineApi<const O extends ApiOptions>(options: O): ApiBuilder<undefined, undefined, undefined, O["stream"] extends true ? true : false, O["static"] extends string[] ? true : false>;
1560
1439
 
1561
- export { type AnyParamRef, type AnySecretRef, type ApiConfig, type ApiHandler, type ApiTokenStrategy, type AppConfig, type AppHandler, type Auth, type AuthConfig, type AuthHelpers, type BucketClient, type BucketConfig, type BucketEvent, type BucketHandler, type ContentType, type Duration, type EffortlessConfig, type EmailClient, type FailedRecord, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, type HttpMethod, type HttpRequest, type HttpResponse, type LambdaConfig, type LambdaWithPermissions, type LogLevel, type MailerConfig, type MailerHandler, type MiddlewareDeny, type MiddlewareHandler, type MiddlewareRedirect, type MiddlewareRequest, type MiddlewareResult, type ParamRef, type Permission, type PutInput, type PutOptions, type QueryByTagParams, type QueryParams, type QueueClient, type ResponseStream, type SecretRef, type SendEmailOptions, type SendMessageInput, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StaticSiteSeo, type StreamView, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type UpdateActions, defineApi, defineApp, defineAuth, defineBucket, defineConfig, defineFifoQueue, defineMailer, defineStaticSite, defineTable, generateBase64, generateHex, generateUuid, param, result, secret, toSeconds, unsafeAs };
1440
+ export { type AnyParamRef, type AnySecretRef, type ApiAuthConfig, type ApiConfig, type ApiHandler, type ApiRoutes, type AppConfig, type AppHandler, type AuthHelpers, type BucketClient, type BucketConfig, type BucketEvent, type BucketHandler, type ConfigHelpers, type ContentType, type DefineSecretFn, type Duration, type EffortlessConfig, type EmailClient, type FifoQueueConfig, type FifoQueueHandler, type FifoQueueMessage, type GenerateSpec, type HttpMethod$1 as HttpMethod, type HttpRequest, type HttpResponse, type LambdaConfig, type LambdaWithPermissions, type LogLevel, type MailerConfig, type MailerHandler, type MiddlewareDeny, type MiddlewareHandler, type MiddlewareRedirect, type MiddlewareRequest, type MiddlewareResult, type ParamRef, type Permission, type PutInput, type PutOptions, type QueryByTagParams, type QueryParams, type QueueClient, type ResponseStream, type SecretRef, type SendEmailOptions, type SendMessageInput, type SkCondition, type StaticFiles, type StaticSiteConfig, type StaticSiteHandler, type StaticSiteSeo, type StreamView, type TableClient, type TableConfig, type TableHandler, type TableItem, type TableKey, type TableRecord, type UpdateActions, defineApi, defineApp, defineBucket, defineConfig, defineFifoQueue, defineMailer, defineSecret, defineStaticSite, defineTable, generateBase64, generateHex, generateUuid, param, secret, toSeconds, unsafeAs };