effortless-aws 0.29.0 → 0.31.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
@@ -1,17 +1,7 @@
1
1
  /**
2
2
  * Configuration for an Effortless project.
3
3
  *
4
- * @example
5
- * ```typescript
6
- * // effortless.config.ts
7
- * import { defineConfig } from "effortless-aws";
8
- *
9
- * export default defineConfig({
10
- * name: "my-service",
11
- * region: "eu-central-1",
12
- * handlers: "src",
13
- * });
14
- * ```
4
+ * @see {@link https://effortless-aws.website/configuration | Configuration guide}
15
5
  */
16
6
  type EffortlessConfig = {
17
7
  /**
@@ -78,21 +68,7 @@ type EffortlessConfig = {
78
68
  runtime?: string;
79
69
  };
80
70
  };
81
- /**
82
- * Helper function for type-safe configuration.
83
- * Returns the config object as-is, but provides TypeScript autocompletion.
84
- *
85
- * @example
86
- * ```typescript
87
- * import { defineConfig } from "effortless-aws";
88
- *
89
- * export default defineConfig({
90
- * name: "my-service",
91
- * region: "eu-central-1",
92
- * handlers: "src",
93
- * });
94
- * ```
95
- */
71
+ /** Helper function for type-safe configuration with TypeScript autocompletion. */
96
72
  declare const defineConfig: (config: EffortlessConfig) => EffortlessConfig;
97
73
 
98
74
  /** Generator spec for auto-creating secrets at deploy time. */
@@ -136,6 +112,20 @@ type LambdaWithPermissions = LambdaConfig & {
136
112
  /** Additional IAM permissions for the Lambda */
137
113
  permissions?: Permission[];
138
114
  };
115
+ /**
116
+ * Lambda configuration passed as argument to `.setup()`.
117
+ * Common across all handler types that create a Lambda function.
118
+ */
119
+ type LambdaOptions = {
120
+ /** Lambda memory in MB (default: 256) */
121
+ memory?: number;
122
+ /** Lambda timeout (default: 30s). Accepts seconds or duration string: `"30s"`, `"5m"` */
123
+ timeout?: Duration;
124
+ /** Additional IAM permissions for the Lambda */
125
+ permissions?: Permission[];
126
+ /** Logging verbosity: "error" (errors only), "info" (+ execution summary), "debug" (+ input/output). Default: "info" */
127
+ logLevel?: LogLevel;
128
+ };
139
129
  type AnySecretRef = SecretRef<any>;
140
130
  /**
141
131
  * Reference to an SSM Parameter Store secret.
@@ -236,127 +226,6 @@ type PutInput<T> = {
236
226
  data: T;
237
227
  ttl?: number;
238
228
  };
239
- /**
240
- * Create a schema function that casts input to T without runtime validation.
241
- * Use when you need T inference alongside other generics (deps, config).
242
- * For handlers without deps/config, prefer `defineTable<Order>({...})`.
243
- * For untrusted input, prefer a real parser (Zod, Effect Schema).
244
- *
245
- * @example
246
- * ```typescript
247
- * export const orders = defineTable({
248
- * schema: unsafeAs<Order>(),
249
- * deps: () => ({ notifications }),
250
- * onRecord: async ({ record, deps }) => { ... }
251
- * });
252
- * ```
253
- */
254
- declare function unsafeAs<T>(): (input: unknown) => T;
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
229
 
361
230
  /** HTTP methods supported by Lambda Function URLs */
362
231
  type HttpMethod$1 = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "ANY";
@@ -411,13 +280,6 @@ type HttpResponse = {
411
280
  */
412
281
  binary?: boolean;
413
282
  };
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
283
  /** Stream helper injected into route args when `stream: true` is set on defineApi */
422
284
  type ResponseStream = {
423
285
  /** Write a raw string chunk to the response stream */
@@ -470,7 +332,12 @@ type BucketClient = {
470
332
  */
471
333
  type BucketConfig = {
472
334
  /** Lambda function settings (memory, timeout, permissions, etc.) */
473
- lambda?: LambdaWithPermissions;
335
+ lambda?: {
336
+ memory?: number;
337
+ timeout?: Duration;
338
+ logLevel?: LogLevel;
339
+ permissions?: Permission[];
340
+ };
474
341
  /** S3 key prefix filter for event notifications (e.g., "uploads/") */
475
342
  prefix?: string;
476
343
  /** S3 key suffix filter for event notifications (e.g., ".jpg") */
@@ -494,79 +361,29 @@ type BucketEvent = {
494
361
  bucketName: string;
495
362
  };
496
363
  /** Spread ctx into callback args (empty when no setup) */
497
- type SpreadCtx$3<C> = [C] extends [undefined] ? {} : C & {};
364
+ type SpreadCtx$4<C> = [C] extends [undefined] ? {} : C & {};
365
+ /** Setup factory — receives bucket/deps/config/files based on what was declared */
366
+ type SetupArgs$4<D, P, HasFiles extends boolean> = {
367
+ bucket: BucketClient;
368
+ } & ([D] extends [undefined] ? {} : {
369
+ deps: ResolveDeps<D>;
370
+ }) & ([P] extends [undefined] ? {} : {
371
+ config: ResolveConfig<P & {}>;
372
+ }) & (HasFiles extends true ? {
373
+ files: StaticFiles;
374
+ } : {});
498
375
  /**
499
376
  * Callback function type for S3 ObjectCreated events
500
377
  */
501
378
  type BucketObjectCreatedFn<C = undefined> = (args: {
502
379
  event: BucketEvent;
503
- } & SpreadCtx$3<C>) => Promise<void>;
380
+ } & SpreadCtx$4<C>) => Promise<void>;
504
381
  /**
505
382
  * Callback function type for S3 ObjectRemoved events
506
383
  */
507
384
  type BucketObjectRemovedFn<C = undefined> = (args: {
508
385
  event: BucketEvent;
509
- } & 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>;
386
+ } & SpreadCtx$4<C>) => Promise<void>;
570
387
  /**
571
388
  * Internal handler object created by defineBucket
572
389
  * @internal
@@ -575,7 +392,7 @@ type BucketHandler<C = any> = {
575
392
  readonly __brand: "effortless-bucket";
576
393
  readonly __spec: BucketConfig;
577
394
  readonly onError?: (...args: any[]) => any;
578
- readonly onAfterInvoke?: (...args: any[]) => any;
395
+ readonly onCleanup?: (...args: any[]) => any;
579
396
  readonly setup?: (...args: any[]) => C | Promise<C>;
580
397
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
581
398
  readonly config?: Record<string, unknown>;
@@ -583,42 +400,54 @@ type BucketHandler<C = any> = {
583
400
  readonly onObjectCreated?: (...args: any[]) => any;
584
401
  readonly onObjectRemoved?: (...args: any[]) => any;
585
402
  };
403
+ /** Options passed to `defineBucket()` — static config */
404
+ type BucketOptions = {
405
+ /** S3 key prefix filter for event notifications (e.g., "uploads/") */
406
+ prefix?: string;
407
+ /** S3 key suffix filter for event notifications (e.g., ".jpg") */
408
+ suffix?: string;
409
+ };
410
+ interface BucketBuilder<D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
411
+ /** Declare handler dependencies */
412
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): BucketBuilder<D2, P, C, HasFiles>;
413
+ /** Declare SSM secrets */
414
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): BucketBuilder<D, P2, C, HasFiles>;
415
+ /** Include static files in the Lambda ZIP */
416
+ include(glob: string): BucketBuilder<D, P, C, true>;
417
+ /** Initialize shared state on cold start with lambda options */
418
+ setup(lambda: LambdaOptions): BucketBuilder<D, P, C, HasFiles>;
419
+ /** Initialize shared state on cold start. Receives bucket (self-client), deps, config, files. */
420
+ setup<C2>(fn: (args: SetupArgs$4<D, P, HasFiles>) => C2 | Promise<C2>): BucketBuilder<D, P, C2, HasFiles>;
421
+ /** Initialize shared state on cold start with lambda options. Receives bucket (self-client), deps, config, files. */
422
+ setup<C2>(fn: (args: SetupArgs$4<D, P, HasFiles>) => C2 | Promise<C2>, lambda: LambdaOptions): BucketBuilder<D, P, C2, HasFiles>;
423
+ /** Handle errors thrown by callbacks */
424
+ onError(fn: (args: {
425
+ error: unknown;
426
+ } & SpreadCtx$4<C>) => void): BucketBuilder<D, P, C, HasFiles>;
427
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
428
+ onCleanup(fn: (args: SpreadCtx$4<C>) => void | Promise<void>): BucketBuilder<D, P, C, HasFiles>;
429
+ /** Handle S3 ObjectCreated events (terminal — returns finalized handler) */
430
+ onObjectCreated(fn: BucketObjectCreatedFn<C>): BucketHandler<C>;
431
+ /** Handle S3 ObjectRemoved events (terminal — returns finalized handler) */
432
+ onObjectRemoved(fn: BucketObjectRemovedFn<C>): BucketHandler<C>;
433
+ /** Finalize as resource-only bucket (no Lambda) */
434
+ build(): BucketHandler<C>;
435
+ }
586
436
  /**
587
437
  * Define an S3 bucket with optional event handlers.
588
438
  *
589
- * Creates an S3 bucket. When event handlers are provided, also creates a Lambda
590
- * function triggered by S3 event notifications.
439
+ * @see {@link https://effortless-aws.website/use-cases/storage | Storage guide}
591
440
  *
592
- * @example Bucket with event handler
593
- * ```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
- * ```
603
- *
604
- * @example Resource-only bucket (no Lambda)
605
- * ```typescript
606
- * export const assets = defineBucket({});
607
- * ```
608
- *
609
- * @example As a dependency
441
+ * @example
610
442
  * ```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
- * });
443
+ * export const uploads = defineBucket({ prefix: "images/", suffix: ".jpg" })
444
+ * .onObjectCreated(async ({ event, bucket }) => {
445
+ * console.log("New upload:", event.key);
446
+ * })
619
447
  * ```
620
448
  */
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>;
449
+ declare function defineBucket(): BucketBuilder;
450
+ declare function defineBucket(options: BucketOptions): BucketBuilder;
622
451
 
623
452
  /**
624
453
  * Configuration options for defining a mailer (SES email identity)
@@ -644,6 +473,8 @@ type MailerHandler = {
644
473
  * On first deploy, DKIM DNS records are printed to the console.
645
474
  * Add them to your DNS provider to verify the domain.
646
475
  *
476
+ * @see {@link https://effortless-aws.website/use-cases/email | Email guide}
477
+ *
647
478
  * @param options - Mailer configuration with the domain to send from
648
479
  * @returns Handler object used by the deployment system and as a `deps` value
649
480
  *
@@ -703,7 +534,12 @@ type FifoQueueMessage<T = unknown> = {
703
534
  */
704
535
  type FifoQueueConfig = {
705
536
  /** Lambda function settings (memory, timeout, permissions, etc.) */
706
- lambda?: LambdaWithPermissions;
537
+ lambda?: {
538
+ memory?: number;
539
+ timeout?: Duration;
540
+ logLevel?: LogLevel;
541
+ permissions?: Permission[];
542
+ };
707
543
  /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */
708
544
  batchSize?: number;
709
545
  /** Maximum time to gather messages before invoking (default: 0). Accepts `"5s"`, `"1m"`, etc. */
@@ -719,26 +555,23 @@ type FifoQueueConfig = {
719
555
  /** Max number of receives before a message is sent to the dead-letter queue (default: 3) */
720
556
  maxReceiveCount?: number;
721
557
  };
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] ? {} : {
558
+ /** Spread ctx into callback args (empty when no setup) */
559
+ type SpreadCtx$3<C> = [C] extends [undefined] ? {} : C & {};
560
+ /** Setup factory receives deps/config/files based on what was declared */
561
+ type SetupArgs$3<D, P, HasFiles extends boolean> = ([D] extends [undefined] ? {} : {
727
562
  deps: ResolveDeps<D>;
728
563
  }) & ([P] extends [undefined] ? {} : {
729
564
  config: ResolveConfig<P & {}>;
730
- }) & ([S] extends [undefined] ? {} : {
565
+ }) & (HasFiles extends true ? {
731
566
  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 & {};
567
+ } : {});
735
568
  /**
736
569
  * Per-message handler function.
737
570
  * Called once per message in the batch. Failures are reported individually.
738
571
  */
739
572
  type FifoQueueMessageFn<T = unknown, C = undefined> = (args: {
740
573
  message: FifoQueueMessage<T>;
741
- } & SpreadCtx$2<C>) => Promise<void>;
574
+ } & SpreadCtx$3<C>) => Promise<void>;
742
575
  /**
743
576
  * Batch handler function.
744
577
  * Called once with all messages in the batch.
@@ -746,59 +579,9 @@ type FifoQueueMessageFn<T = unknown, C = undefined> = (args: {
746
579
  */
747
580
  type FifoQueueBatchFn<T = unknown, C = undefined> = (args: {
748
581
  messages: FifoQueueMessage<T>[];
749
- } & SpreadCtx$2<C>) => Promise<void | {
582
+ } & SpreadCtx$3<C>) => Promise<void | {
750
583
  failures: string[];
751
584
  }>;
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
585
  /**
803
586
  * Internal handler object created by defineFifoQueue
804
587
  * @internal
@@ -808,7 +591,7 @@ type FifoQueueHandler<T = unknown, C = any> = {
808
591
  readonly __spec: FifoQueueConfig;
809
592
  readonly schema?: (input: unknown) => T;
810
593
  readonly onError?: (...args: any[]) => any;
811
- readonly onAfterInvoke?: (...args: any[]) => any;
594
+ readonly onCleanup?: (...args: any[]) => any;
812
595
  readonly setup?: (...args: any[]) => C | Promise<C>;
813
596
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
814
597
  readonly config?: Record<string, unknown>;
@@ -816,37 +599,169 @@ type FifoQueueHandler<T = unknown, C = any> = {
816
599
  readonly onMessage?: (...args: any[]) => any;
817
600
  readonly onMessageBatch?: (...args: any[]) => any;
818
601
  };
602
+ /** Options passed to `defineFifoQueue()` — static config */
603
+ type FifoQueueOptions<T> = {
604
+ /** Number of messages per Lambda invocation (1-10 for FIFO, default: 10) */
605
+ batchSize?: number;
606
+ /** Maximum time to gather messages before invoking (default: 0) */
607
+ batchWindow?: Duration;
608
+ /** Visibility timeout (default: max of timeout or 30s) */
609
+ visibilityTimeout?: Duration;
610
+ /** Message retention period (default: "4d") */
611
+ retentionPeriod?: Duration;
612
+ /** Delivery delay for all messages in the queue (default: 0) */
613
+ delay?: Duration;
614
+ /** Enable content-based deduplication (default: true) */
615
+ contentBasedDeduplication?: boolean;
616
+ /** Max number of receives before DLQ (default: 3) */
617
+ maxReceiveCount?: number;
618
+ /** Decode/validate function for the message body */
619
+ schema?: (input: unknown) => T;
620
+ };
621
+ interface FifoQueueBuilder<T = unknown, D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
622
+ /** Declare handler dependencies */
623
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): FifoQueueBuilder<T, D2, P, C, HasFiles>;
624
+ /** Declare SSM secrets */
625
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): FifoQueueBuilder<T, D, P2, C, HasFiles>;
626
+ /** Include static files in the Lambda bundle. Chainable — call multiple times. */
627
+ include(glob: string): FifoQueueBuilder<T, D, P, C, true>;
628
+ /** Configure Lambda settings only (memory, timeout, permissions, etc.) */
629
+ setup(lambda: LambdaOptions): FifoQueueBuilder<T, D, P, C, HasFiles>;
630
+ /** Initialize shared state on cold start. Receives deps, config, files. */
631
+ setup<C2>(fn: (args: SetupArgs$3<D, P, HasFiles>) => C2 | Promise<C2>): FifoQueueBuilder<T, D, P, C2, HasFiles>;
632
+ /** Initialize shared state on cold start + configure Lambda settings. */
633
+ setup<C2>(fn: (args: SetupArgs$3<D, P, HasFiles>) => C2 | Promise<C2>, lambda: LambdaOptions): FifoQueueBuilder<T, D, P, C2, HasFiles>;
634
+ /** Handle errors thrown by message handlers */
635
+ onError(fn: (args: {
636
+ error: unknown;
637
+ } & SpreadCtx$3<C>) => void): FifoQueueBuilder<T, D, P, C, HasFiles>;
638
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
639
+ onCleanup(fn: (args: SpreadCtx$3<C>) => void | Promise<void>): FifoQueueBuilder<T, D, P, C, HasFiles>;
640
+ /** Per-message handler (terminal — returns finalized handler) */
641
+ onMessage(fn: FifoQueueMessageFn<T, C>): FifoQueueHandler<T, C>;
642
+ /** Batch handler (terminal — returns finalized handler) */
643
+ onMessageBatch(fn: FifoQueueBatchFn<T, C>): FifoQueueHandler<T, C>;
644
+ }
819
645
  /**
820
- * Define a FIFO SQS queue with a Lambda message handler
646
+ * Define a FIFO SQS queue with a Lambda message handler.
821
647
  *
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
648
+ * @see {@link https://effortless-aws.website/use-cases/queue | Queue guide}
826
649
  *
827
- * @example Per-message processing
828
- * ```typescript
829
- * type OrderEvent = { orderId: string; action: string };
830
- *
831
- * export const orderQueue = defineFifoQueue<OrderEvent>({
832
- * onMessage: async ({ message }) => {
833
- * console.log("Processing order:", message.body.orderId);
834
- * }
835
- * });
836
- * ```
837
- *
838
- * @example Batch processing with schema
650
+ * @example
839
651
  * ```typescript
840
- * export const notifications = defineFifoQueue({
841
- * schema: (input) => NotificationSchema.parse(input),
842
- * batchSize: 5,
843
- * onMessageBatch: async ({ messages }) => {
652
+ * export const notifications = defineFifoQueue({ batchSize: 5, schema: (i) => NotifSchema.parse(i) })
653
+ * .onMessageBatch(async ({ messages }) => {
844
654
  * await sendAll(messages.map(m => m.body));
845
- * }
846
- * });
655
+ * })
847
656
  * ```
848
657
  */
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>;
658
+ declare function defineFifoQueue<T = unknown>(): FifoQueueBuilder<T>;
659
+ declare function defineFifoQueue<T = unknown>(options: FifoQueueOptions<T>): FifoQueueBuilder<T>;
660
+
661
+ /**
662
+ * Sort key condition for TableClient.query()
663
+ */
664
+ type SkCondition = string | {
665
+ begins_with: string;
666
+ } | {
667
+ gt: string;
668
+ } | {
669
+ gte: string;
670
+ } | {
671
+ lt: string;
672
+ } | {
673
+ lte: string;
674
+ } | {
675
+ between: [string, string];
676
+ };
677
+ /**
678
+ * Query parameters for TableClient.query()
679
+ */
680
+ type QueryParams = {
681
+ /** Partition key value */
682
+ pk: string;
683
+ /** Optional sort key condition */
684
+ sk?: SkCondition;
685
+ /** Maximum number of items to return */
686
+ limit?: number;
687
+ /** Sort order (true = ascending, false = descending) */
688
+ scanIndexForward?: boolean;
689
+ };
690
+ /**
691
+ * Query parameters for TableClient.queryByTag() — cross-partition query via GSI.
692
+ * Uses the built-in `tag-pk-index` GSI (tag as partition key, pk as sort key).
693
+ */
694
+ type QueryByTagParams = {
695
+ /** Tag value (GSI partition key) — the entity type discriminant */
696
+ tag: string;
697
+ /** Optional pk condition (GSI sort key) */
698
+ pk?: SkCondition;
699
+ /** Maximum number of items to return */
700
+ limit?: number;
701
+ /** Sort order (true = ascending, false = descending) */
702
+ scanIndexForward?: boolean;
703
+ };
704
+ /** Extract keys of T whose values are arrays */
705
+ type ArrayKeys<T> = {
706
+ [K in keyof T]: T[K] extends unknown[] ? K : never;
707
+ }[keyof T];
708
+ /** Extract keys of T whose values are numbers */
709
+ type NumberKeys<T> = {
710
+ [K in keyof T]: T[K] extends number ? K : never;
711
+ }[keyof T];
712
+ /**
713
+ * Update actions for TableClient.update()
714
+ *
715
+ * `set`, `append`, and `remove` target fields inside the `data` attribute.
716
+ * Effortless auto-prefixes `data.` in the DynamoDB expression.
717
+ *
718
+ * @typeParam T - Type of the domain data (the `data` attribute)
719
+ */
720
+ type UpdateActions<T> = {
721
+ /** Set domain data fields (inside `data` attribute) */
722
+ set?: Partial<T>;
723
+ /** Atomically increment/decrement numeric fields inside `data` (use negative values to decrement) */
724
+ increment?: Pick<Partial<T>, NumberKeys<T>>;
725
+ /** Append elements to list fields inside `data` (creates the list if it doesn't exist) */
726
+ append?: Pick<Partial<T>, ArrayKeys<T>>;
727
+ /** Remove fields from `data` */
728
+ remove?: (keyof T)[];
729
+ /** Update the top-level `tag` attribute */
730
+ tag?: string;
731
+ /** Update TTL (set number or null to remove) */
732
+ ttl?: number | null;
733
+ };
734
+ /**
735
+ * Typed DynamoDB table client for single-table design.
736
+ *
737
+ * All items follow the `{ pk, sk, tag, data, ttl? }` structure.
738
+ * `T` is the domain data type stored in the `data` attribute.
739
+ *
740
+ * @typeParam T - Type of the domain data
741
+ */
742
+ /**
743
+ * Options for `put()` operation.
744
+ */
745
+ type PutOptions = {
746
+ /** When true, the put fails if an item with the same pk+sk already exists. */
747
+ ifNotExists?: boolean;
748
+ };
749
+ type TableClient<T = Record<string, unknown>> = {
750
+ /** Put an item. Tag is auto-extracted from `data[tagField]`. Use `ifNotExists` to prevent overwrites. */
751
+ put(item: PutInput<T>, options?: PutOptions): Promise<void>;
752
+ /** Get an item by pk + sk */
753
+ get(key: TableKey): Promise<TableItem<T> | undefined>;
754
+ /** Delete an item by pk + sk */
755
+ delete(key: TableKey): Promise<void>;
756
+ /** Update domain data fields without reading the full item */
757
+ update(key: TableKey, actions: UpdateActions<T>): Promise<void>;
758
+ /** Query by partition key with optional sort key condition */
759
+ query(params: QueryParams): Promise<TableItem<T>[]>;
760
+ /** Query by tag across all partitions via GSI (tag-pk-index). */
761
+ queryByTag(params: QueryByTagParams): Promise<TableItem<T>[]>;
762
+ /** The underlying DynamoDB table name */
763
+ tableName: string;
764
+ };
850
765
 
851
766
  /**
852
767
  * Options for sending an email via EmailClient.send()
@@ -924,7 +839,12 @@ type StreamView = "NEW_AND_OLD_IMAGES" | "NEW_IMAGE" | "OLD_IMAGE" | "KEYS_ONLY"
924
839
  */
925
840
  type TableConfig = {
926
841
  /** Lambda function settings (memory, timeout, permissions, etc.) */
927
- lambda?: LambdaWithPermissions;
842
+ lambda?: {
843
+ memory?: number;
844
+ timeout?: Duration;
845
+ logLevel?: LogLevel;
846
+ permissions?: Permission[];
847
+ };
928
848
  /** DynamoDB billing mode (default: "PAY_PER_REQUEST") */
929
849
  billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
930
850
  /** Stream view type - what data to include in stream records (default: "NEW_AND_OLD_IMAGES") */
@@ -968,22 +888,18 @@ type TableRecord<T = Record<string, unknown>> = {
968
888
  /** Approximate timestamp when the modification occurred */
969
889
  timestamp?: number;
970
890
  };
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: {
891
+ /** Setup factory — receives table/deps/config/files based on what was declared */
892
+ type SetupArgs$2<T, D, P, HasFiles extends boolean> = {
977
893
  table: TableClient<T>;
978
894
  } & ([D] extends [undefined] ? {} : {
979
895
  deps: ResolveDeps<D>;
980
896
  }) & ([P] extends [undefined] ? {} : {
981
897
  config: ResolveConfig<P & {}>;
982
- }) & ([S] extends [undefined] ? {} : {
898
+ }) & (HasFiles extends true ? {
983
899
  files: StaticFiles;
984
- })) => C | Promise<C>;
900
+ } : {});
985
901
  /** Spread ctx into callback args (empty when no setup) */
986
- type SpreadCtx$1<C> = [C] extends [undefined] ? {} : C & {};
902
+ type SpreadCtx$2<C> = [C] extends [undefined] ? {} : C & {};
987
903
  /**
988
904
  * Callback function type for processing a single DynamoDB stream record.
989
905
  * Receives the current record and the full batch for context.
@@ -991,7 +907,7 @@ type SpreadCtx$1<C> = [C] extends [undefined] ? {} : C & {};
991
907
  type TableRecordFn<T = Record<string, unknown>, C = undefined> = (args: {
992
908
  record: TableRecord<T>;
993
909
  batch: readonly TableRecord<T>[];
994
- } & SpreadCtx$1<C>) => Promise<void>;
910
+ } & SpreadCtx$2<C>) => Promise<void>;
995
911
  /**
996
912
  * Batch handler function for DynamoDB stream records.
997
913
  * Called once with all records in the batch.
@@ -999,76 +915,13 @@ type TableRecordFn<T = Record<string, unknown>, C = undefined> = (args: {
999
915
  */
1000
916
  type TableBatchFn<T = Record<string, unknown>, C = undefined> = (args: {
1001
917
  records: readonly TableRecord<T>[];
1002
- } & SpreadCtx$1<C>) => Promise<void | {
918
+ } & SpreadCtx$2<C>) => Promise<void | {
1003
919
  failures: string[];
1004
920
  }>;
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
- });
921
+ /** Static config extracted by AST (no runtime callbacks) */
1070
922
  /**
1071
- * Internal handler object created by defineTable
923
+ * Handler object created by defineTable.
924
+ * Used by runtime wrappers and as type annotation for circular deps.
1072
925
  * @internal
1073
926
  */
1074
927
  type TableHandler<T = Record<string, unknown>, C = any> = {
@@ -1076,7 +929,7 @@ type TableHandler<T = Record<string, unknown>, C = any> = {
1076
929
  readonly __spec: TableConfig;
1077
930
  readonly schema?: (input: unknown) => T;
1078
931
  readonly onError?: (...args: any[]) => any;
1079
- readonly onAfterInvoke?: (...args: any[]) => any;
932
+ readonly onCleanup?: (...args: any[]) => any;
1080
933
  readonly setup?: (...args: any[]) => C | Promise<C>;
1081
934
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1082
935
  readonly config?: Record<string, unknown>;
@@ -1084,40 +937,72 @@ type TableHandler<T = Record<string, unknown>, C = any> = {
1084
937
  readonly onRecord?: (...args: any[]) => any;
1085
938
  readonly onRecordBatch?: (...args: any[]) => any;
1086
939
  };
940
+ /** Options passed to `defineTable()` — resource config only, no Lambda settings */
941
+ type TableOptions<T> = {
942
+ /** DynamoDB billing mode (default: "PAY_PER_REQUEST") */
943
+ billingMode?: "PAY_PER_REQUEST" | "PROVISIONED";
944
+ /** Stream view type (default: "NEW_AND_OLD_IMAGES") */
945
+ streamView?: StreamView;
946
+ /** Number of records to process in each Lambda invocation (1-10000, default: 100) */
947
+ batchSize?: number;
948
+ /** Maximum time to gather records before invoking (default: "2s") */
949
+ batchWindow?: Duration;
950
+ /** Where to start reading the stream (default: "LATEST") */
951
+ startingPosition?: "LATEST" | "TRIM_HORIZON";
952
+ /** Number of records to process concurrently within a batch (default: 1) */
953
+ concurrency?: number;
954
+ /** Name of the field in `data` that serves as the entity type discriminant (default: "tag") */
955
+ tagField?: Extract<keyof T, string>;
956
+ /** Decode/validate function for the `data` portion of stream records */
957
+ schema?: (input: unknown) => T;
958
+ };
959
+ interface TableBuilder<T = Record<string, unknown>, D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
960
+ /** Declare handler dependencies (tables, queues, buckets, mailers) */
961
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): TableBuilder<T, D2, P, C, HasFiles>;
962
+ /** Declare SSM secrets */
963
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): TableBuilder<T, D, P2, C, HasFiles>;
964
+ /** Include static files in the Lambda bundle. Chainable — call multiple times. */
965
+ include(glob: string): TableBuilder<T, D, P, C, true>;
966
+ /** Configure Lambda settings only (memory, timeout, permissions, etc.) */
967
+ setup(lambda: LambdaOptions): TableBuilder<T, D, P, C, HasFiles>;
968
+ /** Initialize shared state on cold start. Receives table (self-client), deps, config, files. */
969
+ setup<C2>(fn: (args: SetupArgs$2<T, D, P, HasFiles>) => C2 | Promise<C2>): TableBuilder<T, D, P, C2, HasFiles>;
970
+ /** Initialize shared state on cold start + configure Lambda settings. */
971
+ setup<C2>(fn: (args: SetupArgs$2<T, D, P, HasFiles>) => C2 | Promise<C2>, lambda: LambdaOptions): TableBuilder<T, D, P, C2, HasFiles>;
972
+ /** Handle errors thrown by onRecord/onRecordBatch */
973
+ onError(fn: (args: {
974
+ error: unknown;
975
+ } & SpreadCtx$2<C>) => void): TableBuilder<T, D, P, C, HasFiles>;
976
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
977
+ onCleanup(fn: (args: SpreadCtx$2<C>) => void | Promise<void>): TableBuilder<T, D, P, C, HasFiles>;
978
+ /** Per-record stream handler (terminal — returns finalized handler) */
979
+ onRecord(fn: TableRecordFn<T, C>): TableHandler<T, C>;
980
+ /** Batch stream handler (terminal — returns finalized handler) */
981
+ onRecordBatch(fn: TableBatchFn<T, C>): TableHandler<T, C>;
982
+ /** Finalize as resource-only table (no Lambda) */
983
+ build(): TableHandler<T, C>;
984
+ }
1087
985
  /**
1088
986
  * Define a DynamoDB table with optional stream handler (single-table design).
1089
987
  *
1090
988
  * Creates a table with fixed key schema: `pk (S)` + `sk (S)`, plus `tag (S)`,
1091
989
  * `data (M)`, and `ttl (N)` attributes. TTL is always enabled.
1092
990
  *
1093
- * @example Table with stream handler
991
+ * @see {@link https://effortless-aws.website/use-cases/database | Database guide}
992
+ *
993
+ * @example
1094
994
  * ```typescript
1095
- * export const orders = defineTable<OrderData>()({
1096
- * batchSize: 10,
1097
- * concurrency: 5,
1098
- * setup: ({ table }) => ({ table }),
1099
- * onRecord: async ({ record, batch, table }) => {
995
+ * export const orders = defineTable<OrderData>({ batchSize: 10, concurrency: 5 })
996
+ * .setup(({ table }) => ({ table }))
997
+ * .onRecord(async ({ record, table }) => {
1100
998
  * if (record.eventName === "INSERT") {
1101
999
  * console.log("New order:", record.new?.data);
1102
1000
  * }
1103
- * }
1104
- * });
1105
- * ```
1106
- *
1107
- * @example Table with runtime validation
1108
- * ```typescript
1109
- * export const orders = defineTable<OrderData>()({
1110
- * schema: (input) => OrderSchema.parse(input),
1111
- * onRecord: async ({ record }) => { ... }
1112
- * });
1113
- * ```
1114
- *
1115
- * @example Table only (no Lambda)
1116
- * ```typescript
1117
- * export const users = defineTable()({});
1001
+ * })
1118
1002
  * ```
1119
1003
  */
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>;
1004
+ declare function defineTable<T = Record<string, unknown>>(): TableBuilder<T>;
1005
+ declare function defineTable<T = Record<string, unknown>>(options: TableOptions<T>): TableBuilder<T>;
1121
1006
 
1122
1007
  /**
1123
1008
  * Configuration for deploying an SSR framework (Nuxt, Astro, etc.)
@@ -1161,18 +1046,10 @@ type AppHandler = {
1161
1046
  *
1162
1047
  * For static-only sites (no SSR), use {@link defineStaticSite} instead.
1163
1048
  *
1049
+ * @see {@link https://effortless-aws.website/use-cases/web-app | Web app guide}
1050
+ *
1164
1051
  * @param options - App configuration: server directory, assets directory, optional build command
1165
1052
  * @returns Handler object used by the deployment system
1166
- *
1167
- * @example Nuxt SSR
1168
- * ```typescript
1169
- * export const app = defineApp({
1170
- * build: "nuxt build",
1171
- * server: ".output/server",
1172
- * assets: ".output/public",
1173
- * lambda: { memory: 1024 },
1174
- * });
1175
- * ```
1176
1053
  */
1177
1054
  declare const defineApp: () => (options: AppConfig) => AppHandler;
1178
1055
 
@@ -1249,26 +1126,10 @@ type StaticSiteHandler = {
1249
1126
  /**
1250
1127
  * Deploy a static site via S3 + CloudFront CDN.
1251
1128
  *
1129
+ * @see {@link https://effortless-aws.website/use-cases/web-app | Web app guide}
1130
+ *
1252
1131
  * @param options - Static site configuration: directory, optional SPA mode, build command
1253
1132
  * @returns Handler object used by the deployment system
1254
- *
1255
- * @example Documentation site
1256
- * ```typescript
1257
- * export const docs = defineStaticSite({
1258
- * dir: "dist",
1259
- * build: "npx astro build",
1260
- * });
1261
- * ```
1262
- *
1263
- * @example SPA with client-side routing
1264
- * ```typescript
1265
- * export const app = defineStaticSite({
1266
- * dir: "dist",
1267
- * spa: true,
1268
- * build: "npm run build",
1269
- * });
1270
- * ```
1271
- *
1272
1133
  */
1273
1134
  declare const defineStaticSite: () => (options: StaticSiteConfig) => StaticSiteHandler;
1274
1135
 
@@ -1330,7 +1191,11 @@ type ExtractAuth<C> = C extends {
1330
1191
  auth: ApiAuthConfig<infer A>;
1331
1192
  } ? A : undefined;
1332
1193
  /** Property names reserved by the framework — cannot be used in setup return */
1333
- type ReservedKeys = 'req' | 'input' | 'stream';
1194
+ type ReservedKeys = 'req' | 'input' | 'stream' | 'ok' | 'fail';
1195
+ /** Success response helper: `ok({ data })` → `{ status: 200, body: { data } }` */
1196
+ type OkHelper = (body?: unknown, status?: number) => HttpResponse;
1197
+ /** Error response helper: `fail("message")` → `{ status: 400, body: { error: "message" } }` */
1198
+ type FailHelper = (message: string, status?: number) => HttpResponse;
1334
1199
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
1335
1200
  /** Parsed route definition stored at runtime */
1336
1201
  type RouteEntry = {
@@ -1340,36 +1205,40 @@ type RouteEntry = {
1340
1205
  public?: boolean;
1341
1206
  };
1342
1207
  /** Spread ctx into route args: Omit auth config, add AuthHelpers if present */
1343
- type SpreadCtx<C> = ([C] extends [undefined] ? {} : Omit<C & {}, 'auth'>) & ([ExtractAuth<C>] extends [undefined] ? {} : {
1208
+ type SpreadCtx$1<C> = ([C] extends [undefined] ? {} : Omit<C & {}, 'auth'>) & ([ExtractAuth<C>] extends [undefined] ? {} : {
1344
1209
  auth: AuthHelpers<ExtractAuth<C>>;
1345
1210
  });
1346
1211
  /** Callback args available inside each route — ctx is spread into args */
1347
- type RouteArgs<C, ST> = SpreadCtx<C> & {
1212
+ type RouteArgs<C, ST> = SpreadCtx$1<C> & {
1348
1213
  req: HttpRequest;
1349
1214
  input: unknown;
1215
+ ok: OkHelper;
1216
+ fail: FailHelper;
1350
1217
  } & ([ST] extends [true] ? {
1351
1218
  stream: ResponseStream;
1352
1219
  } : {});
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;
1220
+ /** Route handler function */
1221
+ type RouteHandler<C, ST> = (args: RouteArgs<C, ST>) => Promise<HttpResponse | void> | HttpResponse | void;
1222
+ /** Route options (e.g. public) */
1223
+ type RouteOptions = {
1357
1224
  public?: boolean;
1358
1225
  };
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: {
1226
+ /** Setup factory receives deps/config/files/enableAuth based on what was declared */
1227
+ type SetupArgs$1<D, P, HasFiles extends boolean> = {
1365
1228
  enableAuth: EnableAuth;
1229
+ ok: OkHelper;
1230
+ fail: FailHelper;
1366
1231
  } & ([D] extends [undefined] ? {} : {
1367
1232
  deps: ResolveDeps<D>;
1368
1233
  }) & ([P] extends [undefined] ? {} : {
1369
1234
  config: ResolveConfig<P & {}>;
1370
- }) & ([S] extends [undefined] ? {} : {
1235
+ }) & (HasFiles extends true ? {
1371
1236
  files: StaticFiles;
1372
- })) => ValidateSetupReturn<C> | Promise<ValidateSetupReturn<C>>;
1237
+ } : {});
1238
+ /** Validate that setup return type does not use reserved property names */
1239
+ type ValidateSetupReturn<C> = C & {
1240
+ [K in ReservedKeys]?: never;
1241
+ };
1373
1242
  /** Static config extracted by AST (no runtime callbacks) */
1374
1243
  type ApiConfig = {
1375
1244
  /** Lambda function settings (memory, timeout, permissions, etc.) */
@@ -1379,79 +1248,212 @@ type ApiConfig = {
1379
1248
  /** Enable response streaming. When true, the Lambda Function URL uses RESPONSE_STREAM invoke mode. */
1380
1249
  stream?: boolean;
1381
1250
  };
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
1251
  /** Internal handler object created by defineApi */
1408
1252
  type ApiHandler<C = undefined> = {
1409
1253
  readonly __brand: "effortless-api";
1410
1254
  readonly __spec: ApiConfig;
1411
1255
  readonly onError?: (...args: any[]) => any;
1412
- readonly onAfterInvoke?: (...args: any[]) => any;
1256
+ readonly onCleanup?: (...args: any[]) => any;
1413
1257
  readonly setup?: (...args: any[]) => C | Promise<C>;
1414
1258
  readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1415
1259
  readonly config?: Record<string, unknown>;
1416
1260
  readonly static?: string[];
1417
1261
  readonly routes?: RouteEntry[];
1418
1262
  };
1263
+ /** Options passed to `defineApi()` */
1264
+ type ApiOptions = {
1265
+ /** Base path prefix for all routes (e.g., "/api") */
1266
+ basePath: `/${string}`;
1267
+ /** Enable response streaming. When true, routes receive a `stream` arg for SSE. */
1268
+ stream?: boolean;
1269
+ };
1270
+ /**
1271
+ * Finalized API handler with route-adding methods.
1272
+ * Has `__brand` so CLI discovers it. Each `.get()/.post()` adds a route and returns self.
1273
+ */
1274
+ interface ApiRoutes<C = undefined, ST extends boolean = false> extends ApiHandler<C> {
1275
+ get(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1276
+ post(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1277
+ put(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1278
+ patch(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1279
+ delete(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1280
+ }
1419
1281
  /**
1420
- * Define an API with typed routes.
1282
+ * Builder interface for defining API handlers.
1421
1283
  *
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`.
1284
+ * Each method sets exactly one generic, so inference happens one step at a time.
1285
+ * This prevents cascading type errors when one property has a mistake.
1286
+ */
1287
+ interface ApiBuilder<D = undefined, P = undefined, C = undefined, ST extends boolean = false, HasFiles extends boolean = false> {
1288
+ /** Declare handler dependencies (tables, queues, buckets, mailers) */
1289
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): ApiBuilder<D2, P, C, ST, HasFiles>;
1290
+ /** Declare SSM secrets */
1291
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): ApiBuilder<D, P2, C, ST, HasFiles>;
1292
+ /** Include static files by glob pattern */
1293
+ include(glob: string): ApiBuilder<D, P, C, ST, true>;
1294
+ /** Configure Lambda settings only (no init function) */
1295
+ setup(lambda: LambdaOptions): ApiBuilder<D, P, C, ST, HasFiles>;
1296
+ /** Initialize shared state on cold start. Receives deps/config/files based on what was declared. */
1297
+ setup<C2>(fn: (args: SetupArgs$1<D, P, HasFiles>) => ValidateSetupReturn<C2> | Promise<ValidateSetupReturn<C2>>): ApiBuilder<D, P, C2, ST, HasFiles>;
1298
+ /** Initialize shared state on cold start with Lambda config. */
1299
+ setup<C2>(fn: (args: SetupArgs$1<D, P, HasFiles>) => ValidateSetupReturn<C2> | Promise<ValidateSetupReturn<C2>>, lambda: LambdaOptions): ApiBuilder<D, P, C2, ST, HasFiles>;
1300
+ /** Handle errors thrown by routes */
1301
+ onError(fn: (args: {
1302
+ error: unknown;
1303
+ req: HttpRequest;
1304
+ ok: OkHelper;
1305
+ fail: FailHelper;
1306
+ } & SpreadCtx$1<C>) => HttpResponse): ApiBuilder<D, P, C, ST, HasFiles>;
1307
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
1308
+ onCleanup(fn: (args: SpreadCtx$1<C>) => void | Promise<void>): ApiBuilder<D, P, C, ST, HasFiles>;
1309
+ /** Add a GET route (terminal — returns finalized handler with route methods) */
1310
+ get(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1311
+ /** Add a POST route (terminal) */
1312
+ post(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1313
+ /** Add a PUT route (terminal) */
1314
+ put(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1315
+ /** Add a PATCH route (terminal) */
1316
+ patch(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1317
+ /** Add a DELETE route (terminal) */
1318
+ delete(path: `/${string}`, handler: RouteHandler<C, ST>, options?: RouteOptions): ApiRoutes<C, ST>;
1319
+ }
1320
+ /**
1321
+ * Define an API with typed routes using a builder pattern.
1322
+ *
1323
+ * @see {@link https://effortless-aws.website/use-cases/http-api | HTTP API guide}
1425
1324
  *
1426
1325
  * @example
1427
1326
  * ```typescript
1428
- * export default defineApi({
1429
- * basePath: "/api",
1430
- * deps: () => ({ users }),
1431
- * setup: ({ deps }) => ({
1327
+ * export const api = defineApi({ basePath: "/api", timeout: "30s" })
1328
+ * .deps(() => ({ users }))
1329
+ * .config(({ defineSecret }) => ({ dbUrl: defineSecret() }))
1330
+ * .setup(async ({ deps, config, enableAuth }) => ({
1432
1331
  * 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
- * })
1332
+ * auth: enableAuth<Session>({ secret: config.dbUrl }),
1333
+ * }))
1334
+ * .onError(({ error, fail }) => fail(String(error), 500))
1335
+ * .get("/me", async ({ users, auth, ok }) => ok(auth.session))
1336
+ * .post("/login", async ({ auth, ok }) => ok(await auth.createSession()), { public: true })
1337
+ * ```
1338
+ */
1339
+ declare function defineApi<const O extends ApiOptions>(options: O): ApiBuilder<undefined, undefined, undefined, O["stream"] extends true ? true : false, false>;
1340
+
1341
+ /**
1342
+ * All IANA time zones.
1343
+ * Generated from Intl.supportedValuesOf("timeZone") on Node.js 22.
1344
+ */
1345
+ type Timezone = "UTC" | "Africa/Abidjan" | "Africa/Accra" | "Africa/Addis_Ababa" | "Africa/Algiers" | "Africa/Asmera" | "Africa/Bamako" | "Africa/Bangui" | "Africa/Banjul" | "Africa/Bissau" | "Africa/Blantyre" | "Africa/Brazzaville" | "Africa/Bujumbura" | "Africa/Cairo" | "Africa/Casablanca" | "Africa/Ceuta" | "Africa/Conakry" | "Africa/Dakar" | "Africa/Dar_es_Salaam" | "Africa/Djibouti" | "Africa/Douala" | "Africa/El_Aaiun" | "Africa/Freetown" | "Africa/Gaborone" | "Africa/Harare" | "Africa/Johannesburg" | "Africa/Juba" | "Africa/Kampala" | "Africa/Khartoum" | "Africa/Kigali" | "Africa/Kinshasa" | "Africa/Lagos" | "Africa/Libreville" | "Africa/Lome" | "Africa/Luanda" | "Africa/Lubumbashi" | "Africa/Lusaka" | "Africa/Malabo" | "Africa/Maputo" | "Africa/Maseru" | "Africa/Mbabane" | "Africa/Mogadishu" | "Africa/Monrovia" | "Africa/Nairobi" | "Africa/Ndjamena" | "Africa/Niamey" | "Africa/Nouakchott" | "Africa/Ouagadougou" | "Africa/Porto-Novo" | "Africa/Sao_Tome" | "Africa/Tripoli" | "Africa/Tunis" | "Africa/Windhoek" | "America/Adak" | "America/Anchorage" | "America/Anguilla" | "America/Antigua" | "America/Araguaina" | "America/Argentina/La_Rioja" | "America/Argentina/Rio_Gallegos" | "America/Argentina/Salta" | "America/Argentina/San_Juan" | "America/Argentina/San_Luis" | "America/Argentina/Tucuman" | "America/Argentina/Ushuaia" | "America/Aruba" | "America/Asuncion" | "America/Bahia" | "America/Bahia_Banderas" | "America/Barbados" | "America/Belem" | "America/Belize" | "America/Blanc-Sablon" | "America/Boa_Vista" | "America/Bogota" | "America/Boise" | "America/Buenos_Aires" | "America/Cambridge_Bay" | "America/Campo_Grande" | "America/Cancun" | "America/Caracas" | "America/Catamarca" | "America/Cayenne" | "America/Cayman" | "America/Chicago" | "America/Chihuahua" | "America/Ciudad_Juarez" | "America/Coral_Harbour" | "America/Cordoba" | "America/Costa_Rica" | "America/Coyhaique" | "America/Creston" | "America/Cuiaba" | "America/Curacao" | "America/Danmarkshavn" | "America/Dawson" | "America/Dawson_Creek" | "America/Denver" | "America/Detroit" | "America/Dominica" | "America/Edmonton" | "America/Eirunepe" | "America/El_Salvador" | "America/Fort_Nelson" | "America/Fortaleza" | "America/Glace_Bay" | "America/Godthab" | "America/Goose_Bay" | "America/Grand_Turk" | "America/Grenada" | "America/Guadeloupe" | "America/Guatemala" | "America/Guayaquil" | "America/Guyana" | "America/Halifax" | "America/Havana" | "America/Hermosillo" | "America/Indiana/Knox" | "America/Indiana/Marengo" | "America/Indiana/Petersburg" | "America/Indiana/Tell_City" | "America/Indiana/Vevay" | "America/Indiana/Vincennes" | "America/Indiana/Winamac" | "America/Indianapolis" | "America/Inuvik" | "America/Iqaluit" | "America/Jamaica" | "America/Jujuy" | "America/Juneau" | "America/Kentucky/Monticello" | "America/Kralendijk" | "America/La_Paz" | "America/Lima" | "America/Los_Angeles" | "America/Louisville" | "America/Lower_Princes" | "America/Maceio" | "America/Managua" | "America/Manaus" | "America/Marigot" | "America/Martinique" | "America/Matamoros" | "America/Mazatlan" | "America/Mendoza" | "America/Menominee" | "America/Merida" | "America/Metlakatla" | "America/Mexico_City" | "America/Miquelon" | "America/Moncton" | "America/Monterrey" | "America/Montevideo" | "America/Montserrat" | "America/Nassau" | "America/New_York" | "America/Nome" | "America/Noronha" | "America/North_Dakota/Beulah" | "America/North_Dakota/Center" | "America/North_Dakota/New_Salem" | "America/Ojinaga" | "America/Panama" | "America/Paramaribo" | "America/Phoenix" | "America/Port-au-Prince" | "America/Port_of_Spain" | "America/Porto_Velho" | "America/Puerto_Rico" | "America/Punta_Arenas" | "America/Rankin_Inlet" | "America/Recife" | "America/Regina" | "America/Resolute" | "America/Rio_Branco" | "America/Santarem" | "America/Santiago" | "America/Santo_Domingo" | "America/Sao_Paulo" | "America/Scoresbysund" | "America/Sitka" | "America/St_Barthelemy" | "America/St_Johns" | "America/St_Kitts" | "America/St_Lucia" | "America/St_Thomas" | "America/St_Vincent" | "America/Swift_Current" | "America/Tegucigalpa" | "America/Thule" | "America/Tijuana" | "America/Toronto" | "America/Tortola" | "America/Vancouver" | "America/Whitehorse" | "America/Winnipeg" | "America/Yakutat" | "Antarctica/Casey" | "Antarctica/Davis" | "Antarctica/DumontDUrville" | "Antarctica/Macquarie" | "Antarctica/Mawson" | "Antarctica/McMurdo" | "Antarctica/Palmer" | "Antarctica/Rothera" | "Antarctica/Syowa" | "Antarctica/Troll" | "Antarctica/Vostok" | "Arctic/Longyearbyen" | "Asia/Aden" | "Asia/Almaty" | "Asia/Amman" | "Asia/Anadyr" | "Asia/Aqtau" | "Asia/Aqtobe" | "Asia/Ashgabat" | "Asia/Atyrau" | "Asia/Baghdad" | "Asia/Bahrain" | "Asia/Baku" | "Asia/Bangkok" | "Asia/Barnaul" | "Asia/Beirut" | "Asia/Bishkek" | "Asia/Brunei" | "Asia/Calcutta" | "Asia/Chita" | "Asia/Colombo" | "Asia/Damascus" | "Asia/Dhaka" | "Asia/Dili" | "Asia/Dubai" | "Asia/Dushanbe" | "Asia/Famagusta" | "Asia/Gaza" | "Asia/Hebron" | "Asia/Hong_Kong" | "Asia/Hovd" | "Asia/Irkutsk" | "Asia/Jakarta" | "Asia/Jayapura" | "Asia/Jerusalem" | "Asia/Kabul" | "Asia/Kamchatka" | "Asia/Karachi" | "Asia/Katmandu" | "Asia/Khandyga" | "Asia/Krasnoyarsk" | "Asia/Kuala_Lumpur" | "Asia/Kuching" | "Asia/Kuwait" | "Asia/Macau" | "Asia/Magadan" | "Asia/Makassar" | "Asia/Manila" | "Asia/Muscat" | "Asia/Nicosia" | "Asia/Novokuznetsk" | "Asia/Novosibirsk" | "Asia/Omsk" | "Asia/Oral" | "Asia/Phnom_Penh" | "Asia/Pontianak" | "Asia/Pyongyang" | "Asia/Qatar" | "Asia/Qostanay" | "Asia/Qyzylorda" | "Asia/Rangoon" | "Asia/Riyadh" | "Asia/Saigon" | "Asia/Sakhalin" | "Asia/Samarkand" | "Asia/Seoul" | "Asia/Shanghai" | "Asia/Singapore" | "Asia/Srednekolymsk" | "Asia/Taipei" | "Asia/Tashkent" | "Asia/Tbilisi" | "Asia/Tehran" | "Asia/Thimphu" | "Asia/Tokyo" | "Asia/Tomsk" | "Asia/Ulaanbaatar" | "Asia/Urumqi" | "Asia/Ust-Nera" | "Asia/Vientiane" | "Asia/Vladivostok" | "Asia/Yakutsk" | "Asia/Yekaterinburg" | "Asia/Yerevan" | "Atlantic/Azores" | "Atlantic/Bermuda" | "Atlantic/Canary" | "Atlantic/Cape_Verde" | "Atlantic/Faeroe" | "Atlantic/Madeira" | "Atlantic/Reykjavik" | "Atlantic/South_Georgia" | "Atlantic/St_Helena" | "Atlantic/Stanley" | "Australia/Adelaide" | "Australia/Brisbane" | "Australia/Broken_Hill" | "Australia/Darwin" | "Australia/Eucla" | "Australia/Hobart" | "Australia/Lindeman" | "Australia/Lord_Howe" | "Australia/Melbourne" | "Australia/Perth" | "Australia/Sydney" | "Europe/Amsterdam" | "Europe/Andorra" | "Europe/Astrakhan" | "Europe/Athens" | "Europe/Belgrade" | "Europe/Berlin" | "Europe/Bratislava" | "Europe/Brussels" | "Europe/Bucharest" | "Europe/Budapest" | "Europe/Busingen" | "Europe/Chisinau" | "Europe/Copenhagen" | "Europe/Dublin" | "Europe/Gibraltar" | "Europe/Guernsey" | "Europe/Helsinki" | "Europe/Isle_of_Man" | "Europe/Istanbul" | "Europe/Jersey" | "Europe/Kaliningrad" | "Europe/Kiev" | "Europe/Kirov" | "Europe/Lisbon" | "Europe/Ljubljana" | "Europe/London" | "Europe/Luxembourg" | "Europe/Madrid" | "Europe/Malta" | "Europe/Mariehamn" | "Europe/Minsk" | "Europe/Monaco" | "Europe/Moscow" | "Europe/Oslo" | "Europe/Paris" | "Europe/Podgorica" | "Europe/Prague" | "Europe/Riga" | "Europe/Rome" | "Europe/Samara" | "Europe/San_Marino" | "Europe/Sarajevo" | "Europe/Saratov" | "Europe/Simferopol" | "Europe/Skopje" | "Europe/Sofia" | "Europe/Stockholm" | "Europe/Tallinn" | "Europe/Tirane" | "Europe/Ulyanovsk" | "Europe/Vaduz" | "Europe/Vatican" | "Europe/Vienna" | "Europe/Vilnius" | "Europe/Volgograd" | "Europe/Warsaw" | "Europe/Zagreb" | "Europe/Zurich" | "Indian/Antananarivo" | "Indian/Chagos" | "Indian/Christmas" | "Indian/Cocos" | "Indian/Comoro" | "Indian/Kerguelen" | "Indian/Mahe" | "Indian/Maldives" | "Indian/Mauritius" | "Indian/Mayotte" | "Indian/Reunion" | "Pacific/Apia" | "Pacific/Auckland" | "Pacific/Bougainville" | "Pacific/Chatham" | "Pacific/Easter" | "Pacific/Efate" | "Pacific/Enderbury" | "Pacific/Fakaofo" | "Pacific/Fiji" | "Pacific/Funafuti" | "Pacific/Galapagos" | "Pacific/Gambier" | "Pacific/Guadalcanal" | "Pacific/Guam" | "Pacific/Honolulu" | "Pacific/Kiritimati" | "Pacific/Kosrae" | "Pacific/Kwajalein" | "Pacific/Majuro" | "Pacific/Marquesas" | "Pacific/Midway" | "Pacific/Nauru" | "Pacific/Niue" | "Pacific/Norfolk" | "Pacific/Noumea" | "Pacific/Pago_Pago" | "Pacific/Palau" | "Pacific/Pitcairn" | "Pacific/Ponape" | "Pacific/Port_Moresby" | "Pacific/Rarotonga" | "Pacific/Saipan" | "Pacific/Tahiti" | "Pacific/Tarawa" | "Pacific/Tongatapu" | "Pacific/Truk" | "Pacific/Wake" | "Pacific/Wallis";
1346
+
1347
+ /** Singular/plural unit for rate expressions */
1348
+ type RateUnit = "minute" | "minutes" | "hour" | "hours" | "day" | "days";
1349
+ /**
1350
+ * Rate expression: `rate(1 hour)`, `rate(5 minutes)`, `rate(2 days)`
1351
+ *
1352
+ * Strictly typed — autocomplete and compile-time validation for unit.
1353
+ */
1354
+ type RateExpression = `rate(${number} ${RateUnit})`;
1355
+ /**
1356
+ * Cron expression: `cron(min hour dom month dow year)`
1357
+ *
1358
+ * Not deeply typed (too combinatorial for TS), but the `cron(...)` wrapper is enforced.
1359
+ *
1360
+ * @example
1361
+ * ```
1362
+ * "cron(0 9 * * ? *)" // daily at 9:00 UTC
1363
+ * "cron(0 9 ? * MON-FRI *)" // weekdays at 9:00
1364
+ * "cron(0/15 * * * ? *)" // every 15 minutes
1365
+ * ```
1366
+ */
1367
+ type CronExpression = `cron(${string})`;
1368
+ /**
1369
+ * EventBridge Scheduler schedule expression.
1370
+ *
1371
+ * - **Rate**: `"rate(5 minutes)"`, `"rate(1 hour)"`, `"rate(1 day)"` — strictly typed units
1372
+ * - **Cron**: `"cron(0 9 * * ? *)"` — 6 fields: min hour dom month dow year
1373
+ */
1374
+ type ScheduleExpression = RateExpression | CronExpression;
1375
+ /** Static config extracted at deploy time */
1376
+ type CronConfig = {
1377
+ /** Lambda function settings (memory, timeout, permissions, etc.) */
1378
+ lambda?: LambdaWithPermissions;
1379
+ /** EventBridge Scheduler schedule expression */
1380
+ schedule: ScheduleExpression;
1381
+ /** IANA timezone for the schedule (default: UTC) */
1382
+ timezone?: Timezone;
1383
+ };
1384
+ /** Setup factory — receives deps/config/files based on what was declared */
1385
+ type SetupArgs<D, P, HasFiles extends boolean> = ([D] extends [undefined] ? {} : {
1386
+ deps: ResolveDeps<D>;
1387
+ }) & ([P] extends [undefined] ? {} : {
1388
+ config: ResolveConfig<P & {}>;
1389
+ }) & (HasFiles extends true ? {
1390
+ files: StaticFiles;
1391
+ } : {});
1392
+ /** Spread ctx into callback args (empty when no setup) */
1393
+ type SpreadCtx<C> = [C] extends [undefined] ? {} : C & {};
1394
+ /** Callback function for cron tick */
1395
+ type CronTickFn<C = undefined> = (args: SpreadCtx<C>) => Promise<void> | void;
1396
+ /**
1397
+ * Handler object created by defineCron.
1398
+ * @internal
1399
+ */
1400
+ type CronHandler<C = any> = {
1401
+ readonly __brand: "effortless-cron";
1402
+ readonly __spec: CronConfig;
1403
+ readonly onError?: (...args: any[]) => any;
1404
+ readonly onCleanup?: (...args: any[]) => any;
1405
+ readonly setup?: (...args: any[]) => C | Promise<C>;
1406
+ readonly deps?: Record<string, unknown> | (() => Record<string, unknown>);
1407
+ readonly config?: Record<string, unknown>;
1408
+ readonly static?: string[];
1409
+ readonly onTick?: (...args: any[]) => any;
1410
+ };
1411
+ /** Options passed to `defineCron()` — resource config only */
1412
+ type CronOptions = {
1413
+ /** EventBridge Scheduler schedule expression: `"rate(5 minutes)"` or `"cron(0 9 * * ? *)"` */
1414
+ schedule: ScheduleExpression;
1415
+ /** IANA timezone for the schedule (default: UTC). Full autocomplete for all timezones. */
1416
+ timezone?: Timezone;
1417
+ };
1418
+ interface CronBuilder<D = undefined, P = undefined, C = undefined, HasFiles extends boolean = false> {
1419
+ /** Declare handler dependencies (tables, queues, buckets, mailers) */
1420
+ deps<D2 extends Record<string, AnyDepHandler>>(fn: () => D2): CronBuilder<D2, P, C, HasFiles>;
1421
+ /** Declare SSM secrets */
1422
+ config<P2 extends Record<string, AnySecretRef>>(fn: ConfigFactory<P2>): CronBuilder<D, P2, C, HasFiles>;
1423
+ /** Include static files in the Lambda bundle. Chainable — call multiple times. */
1424
+ include(glob: string): CronBuilder<D, P, C, true>;
1425
+ /** Configure Lambda settings only (memory, timeout, permissions, logLevel) */
1426
+ setup(lambda: LambdaOptions): CronBuilder<D, P, C, HasFiles>;
1427
+ /** Initialize shared state on cold start. Receives deps, config, files. */
1428
+ setup<C2>(fn: (args: SetupArgs<D, P, HasFiles>) => C2 | Promise<C2>): CronBuilder<D, P, C2, HasFiles>;
1429
+ /** Initialize shared state on cold start + configure Lambda settings. */
1430
+ setup<C2>(fn: (args: SetupArgs<D, P, HasFiles>) => C2 | Promise<C2>, lambda: LambdaOptions): CronBuilder<D, P, C2, HasFiles>;
1431
+ /** Handle errors thrown by onTick */
1432
+ onError(fn: (args: {
1433
+ error: unknown;
1434
+ } & SpreadCtx<C>) => void): CronBuilder<D, P, C, HasFiles>;
1435
+ /** Cleanup callback — runs after each invocation, before Lambda freezes */
1436
+ onCleanup(fn: (args: SpreadCtx<C>) => void | Promise<void>): CronBuilder<D, P, C, HasFiles>;
1437
+ /** Tick handler — called on each scheduled invocation (terminal) */
1438
+ onTick(fn: CronTickFn<C>): CronHandler<C>;
1439
+ }
1440
+ /**
1441
+ * Define a cron job — scheduled Lambda invocation via EventBridge Scheduler.
1442
+ *
1443
+ * @example
1444
+ * ```typescript
1445
+ * export const sync = defineCron({ schedule: "cron(0 9 * * ? *)" })
1446
+ * .deps(() => ({ orders }))
1447
+ * .config(({ defineSecret }) => ({ apiKey: defineSecret() }))
1448
+ * .include("templates/*.html")
1449
+ * .setup(async ({ deps, config, files }) => ({
1450
+ * db: deps.orders, key: config.apiKey, tpl: files,
1451
+ * }), { memory: 512 })
1452
+ * .onTick(async ({ db, key, tpl }) => {
1453
+ * const html = tpl.read("templates/report.html")
1454
+ * })
1453
1455
  * ```
1454
1456
  */
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>;
1457
+ declare function defineCron(options: CronOptions): CronBuilder;
1456
1458
 
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 };
1459
+ 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 CronConfig, type CronHandler, 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 Timezone, type UpdateActions, defineApi, defineApp, defineBucket, defineConfig, defineCron, defineFifoQueue, defineMailer, defineSecret, defineStaticSite, defineTable, generateBase64, generateHex, generateUuid, param, secret, toSeconds };