@lafken/resolver
v0.12.12
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, Environment), 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,
type AppStack,
type ResolverType,
LambdaHandler,
lambdaAssets,
} 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);
// Initialize Lambda build assets
lambdaAssets.initializeMetadata({
foldername: metadata.foldername,
filename: metadata.filename,
minify: metadata.minify,
className: metadata.originalName,
methods: handlers.map((h) => h.name),
});
// 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({
prefix: '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 tracking and dependency resolution.
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. |
| principal | string | No | AWS service principal for invoke permission (e.g., apigateway.amazonaws.com). |
| lambda | LambdaProps | No | Lambda-specific configuration (memory, timeout, runtime, services, env, etc.). |
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: | StartExecution, StopExecution, DescribeExecution, GetExecutionHistory |
| kms | kms: | Encrypt, Decrypt, GenerateDataKey, DescribeKey, and more |
| ssm | ssm: | GetParameter, GetParameters, PutParameter, DescribeParameters, and more |
| event | events: | PutEvents, PutRule, DescribeRule, DescribeEventBus |
RoleProps
| Property | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | IAM role name. |
| services | ServicesValues | Yes | Service permissions — either an array of service names or a callback for dynamic resolution. |
| 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 CDKTNConstructwithisGlobal()andisDependent()methods. - Global tracking — Resources registered with
isGlobal()can be retrieved from anywhere usinggetResource().
make(BaseClass)
Creates a subclass that adds resource tracking methods:
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.isGlobal('notifications', 'events-topic');isGlobal(module, id)
Registers a resource instance under a module::id key so other resources can look it up:
topic.isGlobal('notifications', 'events-topic');
// Retrievable as 'notifications::events-topic'isDependent(callback)
Defers configuration that depends on resources not yet created. The callback is invoked during the afterCreate phase via callDependentCallbacks():
lambda.isDependent(() => {
const topic = lafkenResource.getResource('notifications', 'events-topic');
lambda.addOverride('environment.variables.TOPIC_ARN', topic.arn);
});getResource(module, id)
Retrieves a globally registered resource:
const topic = lafkenResource.getResource<SnsTopic>('notifications', 'events-topic');
console.log(topic.arn);callDependentCallbacks()
Resolves all deferred dependencies. Called automatically by the framework after all resolvers have completed their afterCreate phase.
lambdaAssets
lambdaAssets manages the build and bundling pipeline for Lambda functions using Rolldown. It handles code splitting, minification, and asset packaging.
initializeMetadata(props)
Registers metadata for a Lambda source file. Must be called in the resolver's create method before any LambdaHandler instances reference it:
lambdaAssets.initializeMetadata({
foldername: metadata.foldername,
filename: metadata.filename,
minify: metadata.minify,
className: metadata.originalName,
methods: handlers.map((h) => h.name),
});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 and aws-lambda as externals.
| Property | Type | Description |
|---|---|---|
| foldername | string | Source directory containing the handler file. |
| filename | string | Source file name (without extension). |
| minify | boolean | Whether to minify the output. Defaults to true. |
| className | string | Original class name for the build plugin. |
| methods | string[] | Method names to export from the bundle. |
| afterBuild | (outputPath: string) => void | Optional post-build hook. |
Environment
Environment manages Lambda environment variables, supporting static values, dynamic resource references, and SSM Parameter Store resolution.
import { Environment } from '@lafken/resolver';
// Static values
const env = new Environment(scope, 'handler-env', {
TABLE_NAME: 'orders',
REGION: 'us-east-1',
});
// SSM-backed values
const envWithSSM = new Environment(scope, 'handler-env', {
API_KEY: 'SSM::STRING::/config/api-key',
DB_PASSWORD: 'SSM::SECURE_STRING::/config/db-password',
});SSM Parameter Store
Environment variables can resolve values from AWS SSM at deployment time using the syntax:
SSM::{TYPE}::/path/to/parameter| Type | Description |
|---|---|
| SSM::STRING | Resolves an SSM String parameter. |
| SSM::SECURE_STRING | Resolves an SSM SecureString parameter (decrypted at deploy time). |
Dynamic Resource References
Environment values can also be callback functions that reference other resources:
const env = new Environment(scope, 'handler-env', (ctx) => ({
TABLE_ARN: ctx.getResourceValue('database::orders-table', 'arn'),
QUEUE_URL: ctx.getResourceValue('messaging::order-queue', 'url'),
}));The callback receives a GetResourceProps object with getResourceValue(moduleId, property). If any referenced resource is not yet available, resolution is deferred via isDependent().
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
lambdaAssetsto register and build Lambda source code. - Use
lafkenResource.make()to extend any CDKTN construct with global tracking and dependency resolution. - Register your resolver in
createApp({ resolvers: [new YourResolver()] }).
