effortless-aws 0.29.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
@@ -253,111 +253,6 @@ type PutInput<T> = {
253
253
  */
254
254
  declare function unsafeAs<T>(): (input: unknown) => T;
255
255
 
256
- /**
257
- * Sort key condition for TableClient.query()
258
- */
259
- type SkCondition = string | {
260
- begins_with: string;
261
- } | {
262
- gt: string;
263
- } | {
264
- gte: string;
265
- } | {
266
- lt: string;
267
- } | {
268
- lte: string;
269
- } | {
270
- between: [string, string];
271
- };
272
- /**
273
- * Query parameters for TableClient.query()
274
- */
275
- type QueryParams = {
276
- /** Partition key value */
277
- pk: string;
278
- /** Optional sort key condition */
279
- sk?: SkCondition;
280
- /** Maximum number of items to return */
281
- limit?: number;
282
- /** Sort order (true = ascending, false = descending) */
283
- scanIndexForward?: boolean;
284
- };
285
- /**
286
- * Query parameters for TableClient.queryByTag() — cross-partition query via GSI.
287
- * Uses the built-in `tag-pk-index` GSI (tag as partition key, pk as sort key).
288
- */
289
- type QueryByTagParams = {
290
- /** Tag value (GSI partition key) — the entity type discriminant */
291
- tag: string;
292
- /** Optional pk condition (GSI sort key) */
293
- pk?: SkCondition;
294
- /** Maximum number of items to return */
295
- limit?: number;
296
- /** Sort order (true = ascending, false = descending) */
297
- scanIndexForward?: boolean;
298
- };
299
- /** Extract keys of T whose values are arrays */
300
- type ArrayKeys<T> = {
301
- [K in keyof T]: T[K] extends unknown[] ? K : never;
302
- }[keyof T];
303
- /** Extract keys of T whose values are numbers */
304
- type NumberKeys<T> = {
305
- [K in keyof T]: T[K] extends number ? K : never;
306
- }[keyof T];
307
- /**
308
- * Update actions for TableClient.update()
309
- *
310
- * `set`, `append`, and `remove` target fields inside the `data` attribute.
311
- * Effortless auto-prefixes `data.` in the DynamoDB expression.
312
- *
313
- * @typeParam T - Type of the domain data (the `data` attribute)
314
- */
315
- type UpdateActions<T> = {
316
- /** Set domain data fields (inside `data` attribute) */
317
- set?: Partial<T>;
318
- /** Atomically increment/decrement numeric fields inside `data` (use negative values to decrement) */
319
- increment?: Pick<Partial<T>, NumberKeys<T>>;
320
- /** Append elements to list fields inside `data` (creates the list if it doesn't exist) */
321
- append?: Pick<Partial<T>, ArrayKeys<T>>;
322
- /** Remove fields from `data` */
323
- remove?: (keyof T)[];
324
- /** Update the top-level `tag` attribute */
325
- tag?: string;
326
- /** Update TTL (set number or null to remove) */
327
- ttl?: number | null;
328
- };
329
- /**
330
- * Typed DynamoDB table client for single-table design.
331
- *
332
- * All items follow the `{ pk, sk, tag, data, ttl? }` structure.
333
- * `T` is the domain data type stored in the `data` attribute.
334
- *
335
- * @typeParam T - Type of the domain data
336
- */
337
- /**
338
- * Options for `put()` operation.
339
- */
340
- type PutOptions = {
341
- /** When true, the put fails if an item with the same pk+sk already exists. */
342
- ifNotExists?: boolean;
343
- };
344
- type TableClient<T = Record<string, unknown>> = {
345
- /** Put an item. Tag is auto-extracted from `data[tagField]`. Use `ifNotExists` to prevent overwrites. */
346
- put(item: PutInput<T>, options?: PutOptions): Promise<void>;
347
- /** Get an item by pk + sk */
348
- get(key: TableKey): Promise<TableItem<T> | undefined>;
349
- /** Delete an item by pk + sk */
350
- delete(key: TableKey): Promise<void>;
351
- /** Update domain data fields without reading the full item */
352
- update(key: TableKey, actions: UpdateActions<T>): Promise<void>;
353
- /** Query by partition key with optional sort key condition */
354
- query(params: QueryParams): Promise<TableItem<T>[]>;
355
- /** Query by tag across all partitions via GSI (tag-pk-index). */
356
- queryByTag(params: QueryByTagParams): Promise<TableItem<T>[]>;
357
- /** The underlying DynamoDB table name */
358
- tableName: string;
359
- };
360
-
361
256
  /** HTTP methods supported by Lambda Function URLs */
362
257
  type HttpMethod$1 = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY";
363
258
  /** Short content-type aliases for common response formats */
@@ -411,13 +306,6 @@ type HttpResponse = {
411
306
  */
412
307
  binary?: boolean;
413
308
  };
414
- /** Response helpers for defineApi handlers */
415
- declare const result: {
416
- /** Return a JSON response */
417
- json: (body: unknown, status?: number) => HttpResponse;
418
- /** Return a binary response. Accepts a Buffer and converts to base64 automatically. */
419
- binary: (body: Buffer, contentType: string, headers?: Record<string, string>) => HttpResponse;
420
- };
421
309
  /** Stream helper injected into route args when `stream: true` is set on defineApi */
422
310
  type ResponseStream = {
423
311
  /** Write a raw string chunk to the response stream */
@@ -470,7 +358,12 @@ type BucketClient = {
470
358
  */
471
359
  type BucketConfig = {
472
360
  /** Lambda function settings (memory, timeout, permissions, etc.) */
473
- lambda?: LambdaWithPermissions;
361
+ lambda?: {
362
+ memory?: number;
363
+ timeout?: Duration;
364
+ logLevel?: LogLevel;
365
+ permissions?: Permission[];
366
+ };
474
367
  /** S3 key prefix filter for event notifications (e.g., "uploads/") */
475
368
  prefix?: string;
476
369
  /** S3 key suffix filter for event notifications (e.g., ".jpg") */
@@ -495,6 +388,16 @@ type BucketEvent = {
495
388
  };
496
389
  /** Spread ctx into callback args (empty when no setup) */
497
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
+ } : {});
498
401
  /**
499
402
  * Callback function type for S3 ObjectCreated events
500
403
  */
@@ -507,66 +410,6 @@ type BucketObjectCreatedFn<C = undefined> = (args: {
507
410
  type BucketObjectRemovedFn<C = undefined> = (args: {
508
411
  event: BucketEvent;
509
412
  } & SpreadCtx$3<C>) => Promise<void>;
510
- /**
511
- * Setup factory type for bucket handlers.
512
- * Always receives `bucket: BucketClient` (self-client for the handler's own bucket).
513
- * Also receives `deps` and/or `config` when declared.
514
- */
515
- type SetupFactory$3<C, D, P, S extends string[] | undefined = undefined> = (args: {
516
- bucket: BucketClient;
517
- } & ([D] extends [undefined] ? {} : {
518
- deps: ResolveDeps<D>;
519
- }) & ([P] extends [undefined] ? {} : {
520
- config: ResolveConfig<P & {}>;
521
- }) & ([S] extends [undefined] ? {} : {
522
- files: StaticFiles;
523
- })) => C | Promise<C>;
524
- /** Base options shared by all defineBucket variants */
525
- type DefineBucketBase<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = BucketConfig & {
526
- /**
527
- * Error handler called when onObjectCreated or onObjectRemoved throws.
528
- * If not provided, defaults to `console.error`.
529
- */
530
- onError?: (args: {
531
- error: unknown;
532
- } & SpreadCtx$3<C>) => void;
533
- /** Called after each invocation completes, right before Lambda freezes the process */
534
- onAfterInvoke?: (args: SpreadCtx$3<C>) => void | Promise<void>;
535
- /**
536
- * Factory function to initialize shared state for callbacks.
537
- * Called once on cold start, result is cached and reused across invocations.
538
- * Always receives `bucket: BucketClient` (self-client). When deps/config
539
- * are declared, receives them as well.
540
- */
541
- setup?: SetupFactory$3<C, NoInfer<D>, NoInfer<P>, NoInfer<S>>;
542
- /**
543
- * Dependencies on other handlers (tables, buckets, etc.).
544
- * Typed clients are injected into the handler via the `deps` argument.
545
- * Pass a function returning the deps object: `deps: () => ({ uploads })`.
546
- */
547
- deps?: () => D & {};
548
- /**
549
- * SSM Parameter Store parameters.
550
- * Declare with `defineSecret()` helper. Values are fetched and cached at cold start.
551
- */
552
- config?: ConfigFactory<P>;
553
- /**
554
- * Static file glob patterns to bundle into the Lambda ZIP.
555
- * Files are accessible at runtime via the `files` callback argument.
556
- */
557
- static?: S;
558
- };
559
- /** With event handlers (at least one callback) */
560
- type DefineBucketWithHandlers<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {
561
- onObjectCreated?: BucketObjectCreatedFn<C>;
562
- onObjectRemoved?: BucketObjectRemovedFn<C>;
563
- };
564
- /** Resource-only: no Lambda, just creates the bucket */
565
- type DefineBucketResourceOnly<C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineBucketBase<C, D, P, S> & {
566
- onObjectCreated?: never;
567
- onObjectRemoved?: never;
568
- };
569
- type DefineBucketOptions<C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnySecretRef> | undefined = undefined, S extends string[] | undefined = undefined> = DefineBucketWithHandlers<C, D, P, S> | DefineBucketResourceOnly<C, D, P, S>;
570
413
  /**
571
414
  * Internal handler object created by defineBucket
572
415
  * @internal
@@ -575,7 +418,7 @@ type BucketHandler<C = any> = {
575
418
  readonly __brand: "effortless-bucket";
576
419
  readonly __spec: BucketConfig;
577
420
  readonly onError?: (...args: any[]) => any;
578
- readonly onAfterInvoke?: (...args: any[]) => any;
421
+ readonly onCleanup?: (...args: any[]) => any;
579
422
  readonly setup?: (...args: any[]) => C | Promise<C>;
580
423
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
581
424
  readonly config?: Record<string, unknown>;
@@ -583,42 +426,65 @@ type BucketHandler<C = any> = {
583
426
  readonly onObjectCreated?: (...args: any[]) => any;
584
427
  readonly onObjectRemoved?: (...args: any[]) => any;
585
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
+ }
586
466
  /**
587
467
  * Define an S3 bucket with optional event handlers.
588
468
  *
589
- * Creates an S3 bucket. When event handlers are provided, also creates a Lambda
590
- * function triggered by S3 event notifications.
591
- *
592
469
  * @example Bucket with event handler
593
470
  * ```typescript
594
- * export const uploads = defineBucket({
595
- * prefix: "images/",
596
- * suffix: ".jpg",
597
- * onObjectCreated: async ({ event, bucket }) => {
598
- * const file = await bucket.get(event.key);
599
- * console.log("New upload:", event.key, file?.body.length);
600
- * }
601
- * });
602
- * ```
471
+ * export const uploads = defineBucket({ prefix: "images/", suffix: ".jpg" })
472
+ * .onObjectCreated(async ({ event, bucket }) => {
473
+ * console.log("New upload:", event.key);
474
+ * })
603
475
  *
604
- * @example Resource-only bucket (no Lambda)
605
- * ```typescript
606
- * export const assets = defineBucket({});
607
476
  * ```
608
477
  *
609
- * @example As a dependency
478
+ * @example Resource-only bucket (no Lambda)
610
479
  * ```typescript
611
- * export const api = defineApi({
612
- * basePath: "/process",
613
- * deps: { uploads },
614
- * post: async ({ req, deps }) => {
615
- * await deps.uploads.put("output.jpg", buffer);
616
- * return { status: 200, body: "OK" };
617
- * },
618
- * });
480
+ * export const assets = defineBucket().build()
619
481
  * ```
620
482
  */
621
- declare const defineBucket: () => <C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnySecretRef> | 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;
622
488
 
623
489
  /**
624
490
  * Configuration options for defining a mailer (SES email identity)
@@ -703,7 +569,12 @@ type FifoQueueMessage<T = unknown> = {
703
569
  */
704
570
  type FifoQueueConfig = {
705
571
  /** Lambda function settings (memory, timeout, permissions, etc.) */
706
- lambda?: LambdaWithPermissions;
572
+ lambda?: {
573
+ memory?: number;
574
+ timeout?: Duration;
575
+ logLevel?: LogLevel;
576
+ permissions?: Permission[];
577
+ };
707
578
  /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */
708
579
  batchSize?: number;
709
580
  /** Maximum time to gather messages before invoking (default: 0). Accepts `"5s"`, `"1m"`, etc. */
@@ -719,19 +590,16 @@ type FifoQueueConfig = {
719
590
  /** Max number of receives before a message is sent to the dead-letter queue (default: 3) */
720
591
  maxReceiveCount?: number;
721
592
  };
722
- /**
723
- * Setup factory type always receives an args object.
724
- * Args include `deps` and/or `config` when declared (empty `{}` otherwise).
725
- */
726
- 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] ? {} : {
727
597
  deps: ResolveDeps<D>;
728
598
  }) & ([P] extends [undefined] ? {} : {
729
599
  config: ResolveConfig<P & {}>;
730
- }) & ([S] extends [undefined] ? {} : {
600
+ }) & (HasFiles extends true ? {
731
601
  files: StaticFiles;
732
- })) => C | Promise<C>;
733
- /** Spread ctx into callback args (empty when no setup) */
734
- type SpreadCtx$2<C> = [C] extends [undefined] ? {} : C & {};
602
+ } : {});
735
603
  /**
736
604
  * Per-message handler function.
737
605
  * Called once per message in the batch. Failures are reported individually.
@@ -749,56 +617,6 @@ type FifoQueueBatchFn<T = unknown, C = undefined> = (args: {
749
617
  } & SpreadCtx$2<C>) => Promise<void | {
750
618
  failures: string[];
751
619
  }>;
752
- /** Base options shared by all defineFifoQueue variants */
753
- type DefineFifoQueueBase<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = FifoQueueConfig & {
754
- /**
755
- * Decode/validate function for the message body.
756
- * Called with the JSON-parsed body; should return typed data or throw on validation failure.
757
- */
758
- schema?: (input: unknown) => T;
759
- /**
760
- * Error handler called when onMessage or onMessageBatch throws.
761
- * If not provided, defaults to `console.error`.
762
- */
763
- onError?: (args: {
764
- error: unknown;
765
- } & SpreadCtx$2<C>) => void;
766
- /** Called after each invocation completes, right before Lambda freezes the process */
767
- onAfterInvoke?: (args: SpreadCtx$2<C>) => void | Promise<void>;
768
- /**
769
- * Factory function to initialize shared state for the handler.
770
- * Called once on cold start, result is cached and reused across invocations.
771
- * When deps/params are declared, receives them as argument.
772
- */
773
- setup?: SetupFactory$2<C, NoInfer<D>, NoInfer<P>, NoInfer<S>>;
774
- /**
775
- * Dependencies on other handlers (tables, queues, etc.).
776
- * Typed clients are injected into the handler via the `deps` argument.
777
- * Pass a function returning the deps object: `deps: () => ({ orders })`.
778
- */
779
- deps?: () => D & {};
780
- /**
781
- * SSM Parameter Store parameters.
782
- * Declare with `defineSecret()` helper. Values are fetched and cached at cold start.
783
- */
784
- config?: ConfigFactory<P>;
785
- /**
786
- * Static file glob patterns to bundle into the Lambda ZIP.
787
- * Files are accessible at runtime via the `files` callback argument.
788
- */
789
- static?: S;
790
- };
791
- /** Per-message processing */
792
- type DefineFifoQueueWithOnMessage<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {
793
- onMessage: FifoQueueMessageFn<T, C>;
794
- onMessageBatch?: never;
795
- };
796
- /** Batch processing: all messages at once */
797
- type DefineFifoQueueWithOnBatch<T = unknown, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueBase<T, C, D, P, S> & {
798
- onMessageBatch: FifoQueueBatchFn<T, C>;
799
- onMessage?: never;
800
- };
801
- type DefineFifoQueueOptions<T = unknown, C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnySecretRef> | undefined = undefined, S extends string[] | undefined = undefined> = DefineFifoQueueWithOnMessage<T, C, D, P, S> | DefineFifoQueueWithOnBatch<T, C, D, P, S>;
802
620
  /**
803
621
  * Internal handler object created by defineFifoQueue
804
622
  * @internal
@@ -808,7 +626,7 @@ type FifoQueueHandler<T = unknown, C = any> = {
808
626
  readonly __spec: FifoQueueConfig;
809
627
  readonly schema?: (input: unknown) => T;
810
628
  readonly onError?: (...args: any[]) => any;
811
- readonly onAfterInvoke?: (...args: any[]) => any;
629
+ readonly onCleanup?: (...args: any[]) => any;
812
630
  readonly setup?: (...args: any[]) => C | Promise<C>;
813
631
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
814
632
  readonly config?: Record<string, unknown>;
@@ -816,37 +634,184 @@ type FifoQueueHandler<T = unknown, C = any> = {
816
634
  readonly onMessage?: (...args: any[]) => any;
817
635
  readonly onMessageBatch?: (...args: any[]) => any;
818
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
+ }
819
684
  /**
820
- * Define a FIFO SQS queue with a Lambda message handler
821
- *
822
- * Creates:
823
- * - SQS FIFO queue (with `.fifo` suffix)
824
- * - Lambda function triggered by the queue
825
- * - Event source mapping with partial batch failure support
685
+ * Define a FIFO SQS queue with a Lambda message handler.
826
686
  *
827
687
  * @example Per-message processing
828
688
  * ```typescript
829
- * type OrderEvent = { orderId: string; action: string };
830
- *
831
- * export const orderQueue = defineFifoQueue<OrderEvent>({
832
- * onMessage: async ({ message }) => {
689
+ * export const orderQueue = defineFifoQueue<OrderEvent>()
690
+ * .onMessage(async ({ message }) => {
833
691
  * console.log("Processing order:", message.body.orderId);
834
- * }
835
- * });
692
+ * })
693
+ *
836
694
  * ```
837
695
  *
838
696
  * @example Batch processing with schema
839
697
  * ```typescript
840
- * export const notifications = defineFifoQueue({
841
- * schema: (input) => NotificationSchema.parse(input),
842
- * batchSize: 5,
843
- * onMessageBatch: async ({ messages }) => {
698
+ * export const notifications = defineFifoQueue({ batchSize: 5, schema: (i) => NotifSchema.parse(i) })
699
+ * .onMessageBatch(async ({ messages }) => {
844
700
  * await sendAll(messages.map(m => m.body));
845
- * }
846
- * });
701
+ * })
702
+ *
847
703
  * ```
848
704
  */
849
- declare const defineFifoQueue: <T = unknown>() => <C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnySecretRef> | 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>;
710
+
711
+ /**
712
+ * Sort key condition for TableClient.query()
713
+ */
714
+ type SkCondition = string | {
715
+ begins_with: string;
716
+ } | {
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
+ };
850
815
 
851
816
  /**
852
817
  * Options for sending an email via EmailClient.send()
@@ -924,7 +889,12 @@ type StreamView = "NEW_AND_OLD_IMAGES" | "NEW_IMAGE" | "OLD_IMAGE" | "KEYS_ONLY"
924
889
  */
925
890
  type TableConfig = {
926
891
  /** Lambda function settings (memory, timeout, permissions, etc.) */
927
- lambda?: LambdaWithPermissions;
892
+ lambda?: {
893
+ memory?: number;
894
+ timeout?: Duration;
895
+ logLevel?: LogLevel;
896
+ permissions?: Permission[];
897
+ };
928
898
  /** DynamoDB billing mode (default: "PAY_PER_REQUEST") */
929
899
  billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
930
900
  /** Stream view type - what data to include in stream records (default: "NEW_AND_OLD_IMAGES") */
@@ -968,20 +938,16 @@ type TableRecord<T = Record<string, unknown>> = {
968
938
  /** Approximate timestamp when the modification occurred */
969
939
  timestamp?: number;
970
940
  };
971
- /**
972
- * Setup factory type for table handlers.
973
- * Receives `table: TableClient<T>` (self-client for the handler's own table).
974
- * Also receives `deps` and/or `config` when declared.
975
- */
976
- 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> = {
977
943
  table: TableClient<T>;
978
944
  } & ([D] extends [undefined] ? {} : {
979
945
  deps: ResolveDeps<D>;
980
946
  }) & ([P] extends [undefined] ? {} : {
981
947
  config: ResolveConfig<P & {}>;
982
- }) & ([S] extends [undefined] ? {} : {
948
+ }) & (HasFiles extends true ? {
983
949
  files: StaticFiles;
984
- })) => C | Promise<C>;
950
+ } : {});
985
951
  /** Spread ctx into callback args (empty when no setup) */
986
952
  type SpreadCtx$1<C> = [C] extends [undefined] ? {} : C & {};
987
953
  /**
@@ -1002,73 +968,10 @@ type TableBatchFn<T = Record<string, unknown>, C = undefined> = (args: {
1002
968
  } & SpreadCtx$1<C>) => Promise<void | {
1003
969
  failures: string[];
1004
970
  }>;
1005
- /** Base options shared by all defineTable variants */
1006
- type DefineTableBase<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = Omit<TableConfig, "tagField"> & {
1007
- /** Name of the field in `data` that serves as the entity type discriminant (default: `"tag"`). */
1008
- tagField?: Extract<keyof T, string>;
1009
- /**
1010
- * Decode/validate function for the `data` portion of stream record items.
1011
- * Called with the unmarshalled `data` attribute; should return typed data or throw on validation failure.
1012
- * When provided, T is inferred from the return type — no need to specify generic parameters.
1013
- */
1014
- schema?: (input: unknown) => T;
1015
- /**
1016
- * Error handler called when onRecord/onRecordBatch throws.
1017
- * If not provided, defaults to `console.error`.
1018
- */
1019
- onError?: (args: {
1020
- error: unknown;
1021
- } & SpreadCtx$1<C>) => void;
1022
- /** Called after each invocation completes, right before Lambda freezes the process */
1023
- onAfterInvoke?: (args: SpreadCtx$1<C>) => void | Promise<void>;
1024
- /**
1025
- * Factory function to initialize shared state for callbacks.
1026
- * Called once on cold start, result is cached and reused across invocations.
1027
- * Receives `table` (self-client), plus `deps`/`config`/`files` when declared.
1028
- */
1029
- setup?: SetupFactory$1<C, T, NoInfer<D>, NoInfer<P>, NoInfer<S>>;
1030
- /**
1031
- * Dependencies on other handlers (tables, queues, etc.).
1032
- * Typed clients are injected into setup via the `deps` argument.
1033
- * Pass a function returning the deps object: `deps: () => ({ orders })`.
1034
- */
1035
- deps?: () => D & {};
1036
- /**
1037
- * SSM Parameter Store parameters.
1038
- * Declare with `defineSecret()` helper. Values are fetched and cached at cold start.
1039
- */
1040
- config?: ConfigFactory<P>;
1041
- /**
1042
- * Static file glob patterns to bundle into the Lambda ZIP.
1043
- * Files are accessible at runtime via the `files` argument in setup.
1044
- */
1045
- static?: S;
1046
- };
1047
- /**
1048
- * Options for defineTable.
1049
- * `onRecord` and `onRecordBatch` are mutually exclusive. Both are optional (table-only mode).
1050
- */
1051
- type DefineTableOptions<T = Record<string, unknown>, C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined> = DefineTableBase<T, C, D, P, S> & ({
1052
- /**
1053
- * Per-record stream handler. Called once per record in the batch.
1054
- * Runtime handles partial batch failure reporting automatically.
1055
- * Records are processed with configurable `concurrency` (default: 1 — sequential).
1056
- */
1057
- onRecord?: TableRecordFn<T, C>;
1058
- onRecordBatch?: never;
1059
- } | {
1060
- /**
1061
- * Batch stream handler. Called once with all records in the batch.
1062
- * Return `{ failures: string[] }` with sequence numbers for partial batch failure.
1063
- */
1064
- onRecordBatch?: TableBatchFn<T, C>;
1065
- onRecord?: never;
1066
- } | {
1067
- onRecord?: never;
1068
- onRecordBatch?: never;
1069
- });
971
+ /** Static config extracted by AST (no runtime callbacks) */
1070
972
  /**
1071
- * Internal handler object created by defineTable
973
+ * Handler object created by defineTable.
974
+ * Used by runtime wrappers and as type annotation for circular deps.
1072
975
  * @internal
1073
976
  */
1074
977
  type TableHandler<T = Record<string, unknown>, C = any> = {
@@ -1076,7 +979,7 @@ type TableHandler<T = Record<string, unknown>, C = any> = {
1076
979
  readonly __spec: TableConfig;
1077
980
  readonly schema?: (input: unknown) => T;
1078
981
  readonly onError?: (...args: any[]) => any;
1079
- readonly onAfterInvoke?: (...args: any[]) => any;
982
+ readonly onCleanup?: (...args: any[]) => any;
1080
983
  readonly setup?: (...args: any[]) => C | Promise<C>;
1081
984
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1082
985
  readonly config?: Record<string, unknown>;
@@ -1084,6 +987,55 @@ type TableHandler<T = Record<string, unknown>, C = any> = {
1084
987
  readonly onRecord?: (...args: any[]) => any;
1085
988
  readonly onRecordBatch?: (...args: any[]) => any;
1086
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
+ }
1087
1039
  /**
1088
1040
  * Define a DynamoDB table with optional stream handler (single-table design).
1089
1041
  *
@@ -1092,32 +1044,30 @@ type TableHandler<T = Record<string, unknown>, C = any> = {
1092
1044
  *
1093
1045
  * @example Table with stream handler
1094
1046
  * ```typescript
1095
- * export const orders = defineTable<OrderData>()({
1096
- * batchSize: 10,
1097
- * concurrency: 5,
1098
- * setup: ({ table }) => ({ table }),
1099
- * onRecord: async ({ record, batch, table }) => {
1047
+ * export const orders = defineTable<OrderData>({ batchSize: 10, concurrency: 5 })
1048
+ * .setup(({ table }) => ({ table }))
1049
+ * .onRecord(async ({ record, table }) => {
1100
1050
  * if (record.eventName === "INSERT") {
1101
1051
  * console.log("New order:", record.new?.data);
1102
1052
  * }
1103
- * }
1104
- * });
1053
+ * })
1105
1054
  * ```
1106
1055
  *
1107
- * @example Table with runtime validation
1056
+ * @example Table only (no Lambda)
1108
1057
  * ```typescript
1109
- * export const orders = defineTable<OrderData>()({
1110
- * schema: (input) => OrderSchema.parse(input),
1111
- * onRecord: async ({ record }) => { ... }
1112
- * });
1058
+ * export const users = defineTable<User>().build()
1113
1059
  * ```
1114
1060
  *
1115
- * @example Table only (no Lambda)
1061
+ * @example Table as dependency (resource-only, no Lambda)
1116
1062
  * ```typescript
1117
- * export const users = defineTable()({});
1063
+ * export const sessions = defineTable<Session>().build()
1118
1064
  * ```
1119
1065
  */
1120
- declare const defineTable: <T = Record<string, unknown>>() => <C = undefined, D = undefined, P = undefined, S extends string[] | undefined = undefined>(options: DefineTableOptions<T, C, 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>;
1121
1071
 
1122
1072
  /**
1123
1073
  * Configuration for deploying an SSR framework (Nuxt, Astro, etc.)
@@ -1330,7 +1280,11 @@ type ExtractAuth<C> = C extends {
1330
1280
  auth: ApiAuthConfig<infer A>;
1331
1281
  } ? A : undefined;
1332
1282
  /** Property names reserved by the framework — cannot be used in setup return */
1333
- type ReservedKeys = 'req' | 'input' | 'stream';
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;
1334
1288
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
1335
1289
  /** Parsed route definition stored at runtime */
1336
1290
  type RouteEntry = {
@@ -1347,29 +1301,33 @@ type SpreadCtx<C> = ([C] extends [undefined] ? {} : Omit<C & {}, 'auth'>) & ([Ex
1347
1301
  type RouteArgs<C, ST> = SpreadCtx<C> & {
1348
1302
  req: HttpRequest;
1349
1303
  input: unknown;
1304
+ ok: OkHelper;
1305
+ fail: FailHelper;
1350
1306
  } & ([ST] extends [true] ? {
1351
1307
  stream: ResponseStream;
1352
1308
  } : {});
1353
- /** Route definition with typed args */
1354
- type RouteDefinition<C, ST> = {
1355
- path: `${HttpMethod} /${string}`;
1356
- onRequest: (args: RouteArgs<C, ST>) => Promise<HttpResponse | void> | HttpResponse | void;
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 = {
1357
1313
  public?: boolean;
1358
1314
  };
1359
- /** Validate that setup return type does not use reserved property names */
1360
- type ValidateSetupReturn<C> = C & {
1361
- [K in ReservedKeys]?: never;
1362
- };
1363
- /** Setup factory — receives deps/config/files/enableAuth when declared */
1364
- type SetupFactory<C, D, P, S extends string[] | undefined = undefined> = (args: {
1315
+ /** Setup factory receives deps/config/files/enableAuth based on what was declared */
1316
+ type SetupArgs<D, P, HasFiles extends boolean> = {
1365
1317
  enableAuth: EnableAuth;
1318
+ ok: OkHelper;
1319
+ fail: FailHelper;
1366
1320
  } & ([D] extends [undefined] ? {} : {
1367
1321
  deps: ResolveDeps<D>;
1368
1322
  }) & ([P] extends [undefined] ? {} : {
1369
1323
  config: ResolveConfig<P & {}>;
1370
- }) & ([S] extends [undefined] ? {} : {
1324
+ }) & (HasFiles extends true ? {
1371
1325
  files: StaticFiles;
1372
- })) => ValidateSetupReturn<C> | Promise<ValidateSetupReturn<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
+ };
1373
1331
  /** Static config extracted by AST (no runtime callbacks) */
1374
1332
  type ApiConfig = {
1375
1333
  /** Lambda function settings (memory, timeout, permissions, etc.) */
@@ -1379,79 +1337,104 @@ type ApiConfig = {
1379
1337
  /** Enable response streaming. When true, the Lambda Function URL uses RESPONSE_STREAM invoke mode. */
1380
1338
  stream?: boolean;
1381
1339
  };
1382
- type DefineApiOptions<C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnySecretRef> | undefined = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined> = {
1383
- /** Lambda function settings (memory, timeout, permissions, etc.) */
1384
- lambda?: LambdaWithPermissions;
1385
- /** Base path prefix for all routes (e.g., "/api") */
1386
- basePath: `/${string}`;
1387
- /** Enable response streaming. When true, routes receive a `stream` arg for SSE. */
1388
- stream?: ST;
1389
- /** Factory function to initialize shared state. Called once on cold start. */
1390
- setup?: SetupFactory<C, NoInfer<D>, NoInfer<P>, NoInfer<S>>;
1391
- /** Dependencies on other handlers (tables, queues, etc.): `deps: () => ({ users })` */
1392
- deps?: () => D & {};
1393
- /** SSM Parameter Store parameters. Receives `{ defineSecret }` helper. */
1394
- config?: ConfigFactory<P>;
1395
- /** Static file glob patterns to bundle into the Lambda ZIP */
1396
- static?: S;
1397
- /** Error handler called when a route throws */
1398
- onError?: (args: {
1399
- error: unknown;
1400
- req: HttpRequest;
1401
- } & SpreadCtx<C>) => HttpResponse;
1402
- /** Called after each invocation completes */
1403
- onAfterInvoke?: (args: SpreadCtx<C>) => void | Promise<void>;
1404
- /** Route definitions — plain array of route objects */
1405
- routes?: RouteDefinition<C, ST>[];
1406
- };
1407
1340
  /** Internal handler object created by defineApi */
1408
1341
  type ApiHandler<C = undefined> = {
1409
1342
  readonly __brand: "effortless-api";
1410
1343
  readonly __spec: ApiConfig;
1411
1344
  readonly onError?: (...args: any[]) => any;
1412
- readonly onAfterInvoke?: (...args: any[]) => any;
1345
+ readonly onCleanup?: (...args: any[]) => any;
1413
1346
  readonly setup?: (...args: any[]) => C | Promise<C>;
1414
1347
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1415
1348
  readonly config?: Record<string, unknown>;
1416
1349
  readonly static?: string[];
1417
1350
  readonly routes?: RouteEntry[];
1418
1351
  };
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
+ }
1419
1380
  /**
1420
- * Define an API with typed routes.
1381
+ * Builder interface for defining API handlers.
1421
1382
  *
1422
- * Setup return is spread into route args all properties are directly accessible.
1423
- * Reserved names (`req`, `input`, `stream`) cannot be used in setup return.
1424
- * Auth is configured via an `auth` property in setup return — runtime replaces it with `AuthHelpers`.
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.
1425
1415
  *
1426
1416
  * @example
1427
1417
  * ```typescript
1428
- * export default defineApi({
1429
- * basePath: "/api",
1430
- * deps: () => ({ users }),
1431
- * setup: ({ deps }) => ({
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 }) => ({
1432
1427
  * users: deps.users,
1433
- * auth: {
1434
- * schema: unsafeAs<Session>(),
1435
- * apiToken: {
1436
- * verify: async (value) => {
1437
- * const user = await deps.users.query({ pk: value });
1438
- * return user[0] ? { userId: user[0].sk } : null;
1439
- * },
1440
- * },
1441
- * },
1442
- * }),
1443
- * routes: [
1444
- * {
1445
- * path: "GET /me",
1446
- * onRequest: async ({ users, auth }) => ({
1447
- * status: 200,
1448
- * body: { user: await users.get(auth.session.userId) },
1449
- * }),
1450
- * },
1451
- * ],
1452
- * })
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 })
1453
1433
  * ```
1454
1434
  */
1455
- declare const defineApi: () => <C = undefined, D extends Record<string, AnyDepHandler> | undefined = undefined, P extends Record<string, AnySecretRef> | undefined = undefined, S extends string[] | undefined = undefined, ST extends boolean | undefined = undefined>(options: DefineApiOptions<C, D, P, S, ST>) => ApiHandler<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>;
1456
1439
 
1457
- export { type AnyParamRef, type AnySecretRef, type ApiAuthConfig, type ApiConfig, type ApiHandler, 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, 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 };