@lafken/resolver
v0.16.1
Published
Lafken resolver foundation - AWS Lambda and IAM utilities for building decorator-based infrastructure resolvers
Maintainers
Readme
@lafken/resolver
@lafken/resolver is the foundation package for building custom resolvers within the Lafken framework. It provides the ResolverType interface, infrastructure primitives (LambdaHandler, Role), Lambda asset bundling (initLambdaAssetMetadata/lambdaAssets), and a global resource tracking system (lafkenResource) that enable developers to integrate any AWS service into Lafken.
If you want to create your own resolver — for example, to support a new AWS service or a custom integration — this package gives you everything you need.
How Resolvers Work
Lafken follows a Decorator → Resolver architecture:
- A decorator (built with
@lafken/common) captures metadata about a class or method at build time. - A resolver reads that metadata and generates the corresponding CDKTN infrastructure.
Every resolver implements the ResolverType interface and is registered in createApp(). The framework invokes resolver lifecycle hooks in order:
beforeCreate(scope) → create(module, resource) → afterCreate(scope)beforeCreate— Called once per resolver before any resource is processed. Use it to create shared infrastructure (e.g., an API Gateway, an EventBridge bus).create— Called once per decorated resource. This is where you read metadata and generate the resource's infrastructure.afterCreate— Called once after all resources are processed. Use it to finalize configurations (e.g., wire integrations, build Lambda assets).
Getting Started
This example walks through creating a complete custom resolver for AWS SNS topics.
1. Define the Decorator
Use createResourceDecorator and createLambdaDecorator from @lafken/common to capture metadata:
// src/main/sns.ts
import { createResourceDecorator, createLambdaDecorator } from '@lafken/common';
export const RESOURCE_TYPE = 'SNS' as const;
export interface TopicProps {
name?: string;
}
export interface PublishProps {
name: string;
}
export interface PublishMetadata extends PublishProps {
name: string;
}
// Class decorator — marks a class as an SNS resource
export const Topic = createResourceDecorator<TopicProps>({
type: RESOURCE_TYPE,
callerFileIndex: 5,
});
// Method decorator — marks a method as a handler
export const Publish = (props: PublishProps) =>
createLambdaDecorator<PublishProps, PublishMetadata>({
getLambdaMetadata: (props, methodName) => ({
...props,
name: methodName,
}),
})(props);2. Implement the Resolver
// src/resolver/resolver.ts
import {
type ClassResource,
getResourceMetadata,
getResourceHandlerMetadata,
type ResourceMetadata,
} from '@lafken/common';
import {
type AppModule,
getContextValueByScope,
initLambdaAssetMetadata,
LambdaHandler,
type ResolverType,
} from '@lafken/resolver';
import { SnsTopic } from '@cdktn/provider-aws/lib/sns-topic';
import { SnsTopicSubscription } from '@cdktn/provider-aws/lib/sns-topic-subscription';
import { RESOURCE_TYPE, type PublishMetadata } from '../main/sns';
export class SnsResolver implements ResolverType {
public type = RESOURCE_TYPE;
public create(module: AppModule, resource: ClassResource) {
const metadata: ResourceMetadata = getResourceMetadata(resource);
const handlers = getResourceHandlerMetadata<PublishMetadata>(resource);
const contextBundler = getContextValueByScope(module, 'bundler');
// Register the Lambda source file so `lambdaAssets.createAssets()` bundles it later
initLambdaAssetMetadata({ metadata, handlers, contextBundler });
// Create SNS Topic
const topic = new SnsTopic(module, `${metadata.name}-topic`, {
name: metadata.name,
});
// Create a Lambda + subscription for each handler
for (const handler of handlers) {
const id = `${handler.name}-${metadata.name}`;
const lambda = new LambdaHandler(module, id, {
...handler,
filename: metadata.filename,
foldername: metadata.foldername,
originalName: metadata.originalName,
principal: 'sns.amazonaws.com',
});
new SnsTopicSubscription(module, `${id}-subscription`, {
topicArn: topic.arn,
protocol: 'lambda',
endpoint: lambda.arn,
});
}
}
}3. Register in Your App
import { createApp, createModule } from '@lafken/main';
import { SnsResolver } from './resolver/resolver';
import { NotificationService } from './modules/notifications';
const notifications = createModule({
name: 'notifications',
resources: [NotificationService],
});
createApp({
name: 'my-app',
modules: [notifications],
resolvers: [new SnsResolver()],
});4. Use the Decorators
// src/modules/notifications.ts
import { Topic, Publish } from '../main/sns';
@Topic({ name: 'order-events' })
export class NotificationService {
@Publish({ name: 'order-created' })
onOrderCreated() {
// handler logic
}
@Publish({ name: 'order-shipped' })
onOrderShipped() {
// handler logic
}
}ResolverType Interface
The contract every resolver must implement:
interface ResolverType {
type: string;
beforeCreate?: (scope: AppStack) => Promise<void> | void;
create: (module: AppModule, resource: ClassResource) => Promise<void> | void;
afterCreate?: (scope: AppStack) => Promise<void> | void;
}| Property | Type | Required | Description |
|---|---|---|---|
| type | string | Yes | Unique identifier that matches the type set by the resource decorator. |
| beforeCreate | (scope: AppStack) => void | No | Called once before any resource is processed. Receives the root stack. |
| create | (module: AppModule, resource: ClassResource) => void | Yes | Called for each decorated resource whose type matches this resolver. |
| afterCreate | (scope: AppStack) => void | No | Called once after all resources have been processed. Receives the root stack. |
Lifecycle Parameters
AppStack— The rootTerraformStackwith anidproperty. Available inbeforeCreateandafterCreate.AppModule— A scopedConstructrepresenting the module that contains the resource. Available increate.ClassResource— The decorated class itself. UsegetResourceMetadata()andgetResourceHandlerMetadata()from@lafken/commonto extract metadata.
LambdaHandler
LambdaHandler creates AWS Lambda functions with automatic IAM roles, environment variable management, and context-aware configuration. It extends LambdaFunction from CDKTN via lafkenResource.make(), so it supports global resource tracking.
import { LambdaHandler } from '@lafken/resolver';
new LambdaHandler(module, 'process-order', {
name: 'processOrder',
filename: 'order-handler',
foldername: 'src/handlers',
originalName: 'OrderService',
principal: 'apigateway.amazonaws.com', // optional invoke permission
lambda: {
memory: 256,
timeout: 30,
runtime: 22,
enableTrace: true,
services: ['dynamodb', 's3'],
env: { TABLE_NAME: 'orders' },
},
});Configuration Resolution
LambdaHandler resolves configuration values using a hierarchical precedence:
handler-level > module-level > app-level > defaultThis applies to runtime, timeout, memory, and env. Values set directly on the handler override module-level config, which in turn overrides app-level globals.
LambdaHandlerProps
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Method name used as the Lambda handler entry point. |
| filename | string | Yes | Source file name (without extension) for bundling. |
| foldername | string | Yes | Source directory path for bundling. |
| originalName | string | Yes | Original class name, used for asset generation. |
| suffix | string | No | Appended to the function name for uniqueness. |
| description | string | No | Lambda function description. |
| principal | string | No | AWS service principal for invoke permission (e.g., apigateway.amazonaws.com). |
| sourceArn | string | No | Restricts the invoke permission to a specific source ARN (e.g., a specific S3 bucket or API Gateway resource). Only applies when principal is set. |
| sourceAccount | string | No | Restricts the invoke permission to a specific source AWS account. Only applies when principal is set. |
| lambda | LambdaProps | No | Lambda-specific configuration — see Lambda Configuration (lambda prop) below. |
Lambda Configuration (lambda prop)
LambdaProps (from @lafken/common) covers more than memory/timeout/runtime/services/env/enableTrace shown above. LambdaHandler wires each of these into real CDKTN resources:
| Property | Type | What LambdaHandler does with it |
|---|---|---|
| tags | Record<string, string> | Applied as resource tags on the function. |
| vpcConfig | VpcConfig | Deploys the function inside a VPC (putVpcConfig) so it can reach private resources (RDS, ElastiCache, internal services). |
| ephemeralStorage | number | Sets the /tmp size in MB (512–10240). |
| reservedConcurrency | number | Caps concurrent executions (0 throttles the function entirely). |
| architecture | 'x86_64' \| 'arm64' | Sets the instruction set architecture. |
| alias | AliasConfig | Publishes a version and creates a LambdaAlias pointing to it; if provisionedExecutions > 0, also creates a LambdaProvisionedConcurrencyConfig on that alias. |
| loggingConfig | LoggingConfig | Configures CloudWatch logging (putLoggingConfig); if retentionInDays is set, creates and links a CloudwatchLogGroup. |
| layers | string[] | Layer ARNs to attach. Layers declared at app, module, and handler level are all merged together, not overridden. |
| outputs | ResourceOutputType<LambdaOutputAttributes> | Exports function attributes (arn, invokeArn, qualifiedArn) as SSM parameters or Terraform outputs via ResourceOutput. |
| functionName | string | Overrides the auto-generated function name. |
| ref | string | Registers the function globally (register('lambda', ref)), retrievable elsewhere via lafkenResource.getResource('lambda', ref) or Refs.resourceValue('lambda::<ref>', attr). |
See LambdaProps in @lafken/common's source for the full JSDoc on each field.
Role
Role creates IAM roles with predefined permission sets for common AWS services. It extends IamRole via lafkenResource.make().
import { Role } from '@lafken/resolver';
// Simple: grant default permissions for listed services
new Role(scope, 'service-role', {
name: 'order-processor-role',
services: ['dynamodb', 's3', 'sqs'],
});
// Fine-grained: specify permissions and resources
new Role(scope, 'restricted-role', {
name: 'read-only-role',
services: [
{ type: 'dynamodb', permissions: ['Query', 'GetItem'], resources: ['arn:aws:dynamodb:*:*:table/orders'] },
{ type: 's3', permissions: ['GetObject'], resources: ['arn:aws:s3:::my-bucket/*'] },
],
});Supported Services
Each service name maps to a default set of IAM actions:
| Service | IAM Prefix | Default Actions |
|---|---|---|
| dynamodb | dynamodb: | Query, Scan, GetItem, BatchGetItem, PutItem, DeleteItem, UpdateItem, ConditionCheckItem |
| s3 | s3: | Full CRUD — GetObject, PutObject, DeleteObject, ListBucket, and more |
| lambda | lambda: | InvokeFunction |
| cloudwatch | logs: | CreateLogGroup, CreateLogStream, PutLogEvents, and more |
| sqs | sqs: | SendMessage, ReceiveMessage, DeleteMessage, GetQueueUrl, GetQueueAttributes |
| state_machine | states: | InvokeHTTPEndpoint, DescribeExecution, StartExecution, StopExecution, GetExecutionHistory |
| kms | kms: | Encrypt, Decrypt, GenerateDataKey, DescribeKey, and more |
| ssm | ssm: | GetParameter, GetParameters, PutParameter, DescribeParameters, and more |
| event | events: | DescribeEventRule, DescribeEventBus, DescribeRule, PutEvents, PutRule |
| kinesis | kinesis: | PutRecord, PutRecords |
RoleProps
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | IAM role name. |
| services | Services[] | Yes | Service permissions — an array of service names or fine-grained permission objects. |
| principal | string | No | AWS service principal for AssumeRole. Defaults to lambda.amazonaws.com. |
Custom Service Permissions
For services not in the predefined list, use the custom type:
new Role(scope, 'custom-role', {
name: 'ses-sender-role',
services: [
{ type: 'custom', serviceName: 'ses', permissions: ['SendEmail', 'SendRawEmail'], resources: ['*'] },
],
});lafkenResource
lafkenResource is the global resource registry. It provides two core capabilities:
- Mixin creation —
lafkenResource.make(BaseClass)enhances any CDKTNConstructwith aregister()method. - Global tracking — Resources registered with
register()can be retrieved from anywhere usinggetResource().
make(BaseClass)
Creates a subclass that adds resource tracking methods. BaseClass must extend CDKTN's Construct — this works for any Terraform resource construct, not just the ones Lafken already ships a resolver for:
import { lafkenResource } from '@lafken/resolver';
import { SnsTopic } from '@cdktn/provider-aws/lib/sns-topic';
// Create a trackable version of SnsTopic
class TrackableTopic extends lafkenResource.make(SnsTopic) {}
const topic = new TrackableTopic(scope, 'my-topic', { name: 'events' });
// Register globally so other resolvers can reference it
topic.register('notifications', 'events-topic');register(namespace, id)
Registers a resource instance under a namespace::id key so other resources can look it up:
topic.register('notifications', 'events-topic');
// Retrievable as 'notifications::events-topic'namespace accepts any of the built-in RegisterNamespaces values ('api', 'bucket', 'dynamo', 'queue', 'lambda', ...) or an arbitrary string, so custom/unsupported resources can pick their own namespace.
getResource(module, id)
Retrieves a globally registered resource:
const topic = lafkenResource.getResource<SnsTopic>('notifications', 'events-topic');
console.log(topic.arn);Prefer Refs.resourceValue() (below) over calling getResource() directly for cross-resource values — it returns a deferred token that's safe to embed in config regardless of resource declaration order, whereas getResource() requires the target to already be registered at call time.
reset()
Clears the registry:
lafkenResource.reset();lafkenResource is a module-level singleton, so this is mainly useful in tests — call it in beforeEach() to stop resources registered in one test case from leaking into the next.
Wrapping an arbitrary Terraform resource
Because make() accepts any CDKTN construct, it's the escape hatch for using a Terraform resource Lafken has no dedicated decorator/resolver for. Wrap it, register() it under a namespace of your choice, then read it from anywhere — including a Lambda's env — with Refs.resourceValue('namespace::id', attribute) (see Environment Variables):
// resolver.ts — any resolver's create()/beforeCreate()
import { lafkenResource } from '@lafken/resolver';
import { CloudfrontDistribution } from '@cdktn/provider-aws/lib/cloudfront-distribution';
class TrackableDistribution extends lafkenResource.make(CloudfrontDistribution) {}
const distribution = new TrackableDistribution(module, 'cdn', {
/* ...distribution config... */
});
distribution.register('cdn', 'assets-distribution');// elsewhere — a LambdaHandler's env, in the same or a different resolver
import { Refs } from '@lafken/common';
lambda: {
env: {
DISTRIBUTION_ID: Refs.resourceValue('cdn::assets-distribution', 'id'),
DISTRIBUTION_DOMAIN: Refs.resourceValue('cdn::assets-distribution', 'domainName'),
},
}Refs.resourceValue returns a Lazy CDKTN token that looks the resource up in the registry at synth time, so declaration order between the wrapped resource and the Lambda that references it doesn't matter — as long as both exist by the time createApp() synthesizes the stack.
lambdaAssets
lambdaAssets manages the build and bundling pipeline for Lambda functions using Rolldown. It handles code splitting, minification, and asset packaging. Every built-in resolver (api, queue, event, schedule, state-machine, standalone, pubsub, ...) registers its Lambda source files through the initLambdaAssetMetadata() helper described below rather than calling lambdaAssets directly.
initLambdaAssetMetadata(props)
Exported from @lafken/resolver's utils. Registers metadata for a Lambda source file, merging the resource's own bundler config with the app/module-level one. Call it in the resolver's create method, before any LambdaHandler instances reference the same filename/foldername:
import { getContextValueByScope, initLambdaAssetMetadata } from '@lafken/resolver';
const contextBundler = getContextValueByScope(module, 'bundler');
initLambdaAssetMetadata({
metadata, // ResourceMetadata — provides filename, foldername, originalName, bundler
handlers, // LambdaMetadata[] — provides each handler's name
contextBundler,
streamingByMethod: { onOrderCreated: true }, // optional, per-method response streaming
afterBuild: (outputPath) => { /* optional post-build hook */ },
});| Property | Type | Required | Description |
|---|---|---|---|
| metadata | ResourceMetadata | Yes | The decorated resource's metadata (getResourceMetadata()); provides filename, foldername, originalName, and its own bundler config. |
| handlers | LambdaMetadata[] | Yes | The decorated handlers (getResourceHandlerMetadata()); each contributes its name as an exported method. |
| contextBundler | BundlerConfig | No | App/module-level bundler config (typically getContextValueByScope(module, 'bundler')), used as a fallback when the resource sets no bundler.minify. |
| streamingByMethod | Record<string, boolean> | No | Marks specific handler methods for response streaming (wrapped with awslambda.streamifyResponse at build time). |
| afterBuild | (outputPath: string) => void | No | Called after the asset is bundled, with the output directory path. |
lambdaAssets.initializeMetadata()/addLambda() are the lower-level methods initLambdaAssetMetadata() and LambdaHandler call internally — resolvers should use initLambdaAssetMetadata() instead of calling them directly.
createAssets()
Builds all registered Lambda assets. Called automatically by the framework after all resolvers complete. Each asset is bundled with Rolldown as a CJS module targeting Node.js, with @aws-sdk, aws-lambda, and node:* as externals (plus any externalPackages set on the app/module/resource bundler config).
Environment Variables
Lambda environment variables (EnvironmentValue = Record<string, string>) are passed straight through to the underlying aws_lambda_function resource — there is no dedicated Environment construct or SSM-string convention to parse. Static values are plain strings; dynamic values are produced by calling Refs.resourceValue/Refs.ssmValue (from @lafken/common) directly, since both already return a real, deferred CDKTN token by the time they land in the config:
import { Refs } from '@lafken/common';
lambda: {
env: {
TABLE_NAME: 'orders',
TABLE_ARN: Refs.resourceValue('database::orders-table', 'arn'),
API_KEY: Refs.ssmValue('/config/api-key'),
DB_PASSWORD: Refs.ssmValue('/config/db-password', true), // secure string
},
}These functions are resolved lazily by CDKTN at synth time, regardless of the declaration order between the resources involved — see @lafken/common's Cross-Resource Refss for the full list of available reference functions (Refs.resourceValue, Refs.ssmValue, Refs.accountId, Refs.callerArn, Refs.region, Refs.partition, Refs.dnsSuffix, fn, token) and how registerRefResolvers wires this resolver package's implementation into them.
Context Utilities
createApp()/createModule() store their globalConfig.lambda under CDKTN construct context (app/module context names). These helpers read it back, which is how LambdaHandler's Configuration Resolution hierarchy (handler > module > app > default) is implemented — use them in a custom resolver to honor the same hierarchy for its own config.
import { getAppContext, getModuleContext, getContextValueByScope } from '@lafken/resolver';
// inside a resolver's create(module, resource)
const appContext = getAppContext(module); // GlobalContext set on createApp()
const moduleContext = getModuleContext(module); // GlobalContext set on createModule(), if any
// Or read a single key, falling back from module- to app-level automatically:
const contextBundler = getContextValueByScope(module, 'bundler');| Function | Description |
|---|---|
| getAppContext(scope) | Returns the app-level GlobalContext. |
| getModuleContext(scope) | Returns the module-level GlobalContext, if the module set one. |
| getContextValue(key, appContext, moduleContext) | Returns moduleContext[key] ?? appContext[key]. |
| getContextValueByScope(scope, key) | Combines the three calls above — resolves appContext/moduleContext from scope and returns the module-over-app value for key. |
Testing Utilities
setupTestingStack()
Creates a test-ready CDKTN stack for unit testing resolvers:
import { setupTestingStack } from '@lafken/resolver';
const { app, stack } = setupTestingStack();setupTestingStackWithModule()
Creates a stack with a pre-configured module scope:
import { setupTestingStackWithModule } from '@lafken/resolver';
const { app, stack, module } = setupTestingStackWithModule();enableBuildEnvVariable()
Decorators only capture metadata during builds. In tests, enable build mode first:
import { enableBuildEnvVariable } from '@lafken/common';
describe('SnsResolver', () => {
enableBuildEnvVariable();
// Now decorators will work
@Topic({ name: 'test-topic' })
class TestResource {
@Publish({ name: 'test' })
handler() {}
}
});Building a Custom Resolver — Summary
- Create decorators with
createResourceDecorator()/createLambdaDecorator()from@lafken/common. Set a uniquetypestring. - Implement
ResolverType— settypeto match your decorator, implementcreate()to process resources, optionally usebeforeCreate()/afterCreate()for shared or deferred setup. - Use
LambdaHandlerto create Lambda functions with automatic IAM, context, and environment management. - Use
initLambdaAssetMetadata()to register Lambda source files;lambdaAssets.createAssets()bundles them automatically after all resolvers complete. - Use
lafkenResource.make()to extend any CDKTN construct with global resource tracking. - Register your resolver in
createApp({ resolvers: [new YourResolver()] }).
