@chaim-tools/cdk-lib 0.1.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.
Files changed (39) hide show
  1. package/README.md +238 -0
  2. package/lib/binders/base-chaim-binder.d.ts +144 -0
  3. package/lib/binders/base-chaim-binder.js +532 -0
  4. package/lib/binders/chaim-dynamodb-binder.d.ts +95 -0
  5. package/lib/binders/chaim-dynamodb-binder.js +292 -0
  6. package/lib/config/chaim-endpoints.d.ts +47 -0
  7. package/lib/config/chaim-endpoints.js +51 -0
  8. package/lib/index.d.ts +15 -0
  9. package/lib/index.js +43 -0
  10. package/lib/lambda-handler/.test-temp/snapshot.json +1 -0
  11. package/lib/lambda-handler/handler.js +513 -0
  12. package/lib/lambda-handler/handler.test.ts +365 -0
  13. package/lib/lambda-handler/package-lock.json +1223 -0
  14. package/lib/lambda-handler/package.json +14 -0
  15. package/lib/services/ingestion-service.d.ts +50 -0
  16. package/lib/services/ingestion-service.js +81 -0
  17. package/lib/services/os-cache-paths.d.ts +52 -0
  18. package/lib/services/os-cache-paths.js +123 -0
  19. package/lib/services/schema-service.d.ts +11 -0
  20. package/lib/services/schema-service.js +67 -0
  21. package/lib/services/snapshot-cleanup.d.ts +78 -0
  22. package/lib/services/snapshot-cleanup.js +220 -0
  23. package/lib/types/base-binder-props.d.ts +32 -0
  24. package/lib/types/base-binder-props.js +17 -0
  25. package/lib/types/credentials.d.ts +57 -0
  26. package/lib/types/credentials.js +83 -0
  27. package/lib/types/data-store-metadata.d.ts +67 -0
  28. package/lib/types/data-store-metadata.js +4 -0
  29. package/lib/types/failure-mode.d.ts +16 -0
  30. package/lib/types/failure-mode.js +21 -0
  31. package/lib/types/ingest-contract.d.ts +110 -0
  32. package/lib/types/ingest-contract.js +12 -0
  33. package/lib/types/snapshot-cache-policy.d.ts +52 -0
  34. package/lib/types/snapshot-cache-policy.js +57 -0
  35. package/lib/types/snapshot-payload.d.ts +245 -0
  36. package/lib/types/snapshot-payload.js +3 -0
  37. package/lib/types/table-binding-config.d.ts +43 -0
  38. package/lib/types/table-binding-config.js +57 -0
  39. package/package.json +67 -0
package/README.md ADDED
@@ -0,0 +1,238 @@
1
+ # @chaim-tools/cdk-lib
2
+
3
+ AWS CDK L2 constructs for binding DynamoDB tables to Chaim schemas.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @chaim-tools/cdk-lib
9
+ # or
10
+ pnpm add @chaim-tools/cdk-lib
11
+ ```
12
+
13
+ ## Development
14
+
15
+ ### Build
16
+
17
+ ```bash
18
+ pnpm install
19
+ pnpm build
20
+ ```
21
+
22
+ ### Test
23
+
24
+ ```bash
25
+ pnpm test # Run tests
26
+ pnpm test:watch # Watch mode
27
+ pnpm test:coverage # With coverage
28
+ ```
29
+
30
+ ### Clean
31
+
32
+ ```bash
33
+ pnpm clean
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```typescript
39
+ import { ChaimDynamoDBBinder, ChaimCredentials, TableBindingConfig } from '@chaim-tools/cdk-lib';
40
+ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
41
+
42
+ // Create a DynamoDB table
43
+ const usersTable = new dynamodb.Table(this, 'Users', {
44
+ partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
45
+ billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
46
+ });
47
+
48
+ // Create binding configuration
49
+ const config = new TableBindingConfig(
50
+ 'my-app',
51
+ ChaimCredentials.fromSecretsManager('chaim/api-credentials')
52
+ );
53
+
54
+ // Bind schema to table
55
+ new ChaimDynamoDBBinder(this, 'UsersBinding', {
56
+ schemaPath: './schemas/users.bprint',
57
+ table: usersTable,
58
+ config,
59
+ });
60
+ ```
61
+
62
+ ## API Reference
63
+
64
+ ### `ChaimDynamoDBBinder`
65
+
66
+ Construct that binds a DynamoDB table to a Chaim schema.
67
+
68
+ #### Props
69
+
70
+ | Property | Type | Required | Description |
71
+ |----------|------|----------|-------------|
72
+ | `schemaPath` | string | Yes | Path to `.bprint` schema file |
73
+ | `table` | `ITable` | Yes | DynamoDB table to bind |
74
+ | `config` | `TableBindingConfig` | Yes | Binding configuration (appId, credentials, failureMode) |
75
+
76
+ ### `TableBindingConfig`
77
+
78
+ Configuration for entity bindings. For single-table design, create one config and share across all entity bindings.
79
+
80
+ ```typescript
81
+ // Create config with Secrets Manager (recommended for production)
82
+ const config = new TableBindingConfig(
83
+ 'my-app',
84
+ ChaimCredentials.fromSecretsManager('chaim/api-credentials')
85
+ );
86
+
87
+ // Or with direct API keys (for development)
88
+ const config = new TableBindingConfig(
89
+ 'my-app',
90
+ ChaimCredentials.fromApiKeys(apiKey, apiSecret),
91
+ FailureMode.STRICT // Optional - defaults to BEST_EFFORT
92
+ );
93
+ ```
94
+
95
+ **Constructor Parameters:**
96
+ - `appId` (string) - Application ID for the Chaim platform
97
+ - `credentials` (IChaimCredentials) - API credentials
98
+ - `failureMode` (FailureMode) - Optional, defaults to BEST_EFFORT
99
+
100
+ ### `ChaimCredentials`
101
+
102
+ Factory class for creating Chaim API credentials.
103
+
104
+ ```typescript
105
+ // Using AWS Secrets Manager (recommended for production)
106
+ const credentials = ChaimCredentials.fromSecretsManager('chaim/api-credentials');
107
+
108
+ // Using direct API keys (for development/testing)
109
+ const credentials = ChaimCredentials.fromApiKeys(apiKey, apiSecret);
110
+ ```
111
+
112
+ ### `FailureMode`
113
+
114
+ | Mode | Behavior |
115
+ |------|----------|
116
+ | `BEST_EFFORT` (default) | Log errors, return SUCCESS to CloudFormation |
117
+ | `STRICT` | Return FAILED to CloudFormation on any ingestion error |
118
+
119
+ ## Single-Table Design (Multiple Entities)
120
+
121
+ For single-table design where multiple entity types share one DynamoDB table, create **one** `TableBindingConfig` and share it across all entity bindings:
122
+
123
+ ```typescript
124
+ import { ChaimDynamoDBBinder, ChaimCredentials, TableBindingConfig } from '@chaim-tools/cdk-lib';
125
+
126
+ const singleTable = new dynamodb.Table(this, 'SingleTable', {
127
+ partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING },
128
+ sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING },
129
+ });
130
+
131
+ // Create config ONCE for the table
132
+ const tableConfig = new TableBindingConfig(
133
+ 'my-app',
134
+ ChaimCredentials.fromSecretsManager('chaim/api-credentials')
135
+ );
136
+
137
+ // Share config across all entities in the table
138
+ new ChaimDynamoDBBinder(this, 'UserBinding', {
139
+ schemaPath: './schemas/user.bprint',
140
+ table: singleTable,
141
+ config: tableConfig,
142
+ });
143
+
144
+ new ChaimDynamoDBBinder(this, 'OrderBinding', {
145
+ schemaPath: './schemas/order.bprint',
146
+ table: singleTable,
147
+ config: tableConfig, // Same config!
148
+ });
149
+
150
+ new ChaimDynamoDBBinder(this, 'ProductBinding', {
151
+ schemaPath: './schemas/product.bprint',
152
+ table: singleTable,
153
+ config: tableConfig, // Same config!
154
+ });
155
+ ```
156
+
157
+ **Why this pattern?**
158
+
159
+ All entities in the same DynamoDB table must belong to the same application (`appId`) with the same credentials. `TableBindingConfig` enforces this by design:
160
+
161
+ - Sharing the same config object makes consistency automatic
162
+ - Validation catches accidental misconfigurations (different appIds)
163
+ - Clear intent in your CDK code
164
+ - DRY - define credentials once
165
+
166
+ **Result:**
167
+ - 3 separate snapshots (one per entity)
168
+ - 3 separate resourceIds: `SingleTable__User`, `SingleTable__Order`, `SingleTable__Product`
169
+ - All with the same `appId` and `credentials`
170
+ - Each entity can be independently created, updated, or deleted
171
+
172
+ ## How It Works
173
+
174
+ 1. At **synth time**: The construct reads your `.bprint` file, validates it, and writes a snapshot to the CDK asset directory
175
+ 2. During **deploy**: CloudFormation invokes the ingestion Lambda in your account
176
+ 3. The Lambda:
177
+ - Reads the bundled snapshot from `./snapshot.json`
178
+ - Generates `eventId` (UUID v4), `nonce` (UUID v4), and `contentHash` (SHA-256)
179
+ - Requests presigned URL: `POST /ingest/presign` with HMAC authentication
180
+ - Uploads snapshot: `PUT <presignedUrl>`
181
+
182
+ ## Ingestion Flow
183
+
184
+ ```
185
+ Create/Update:
186
+ 1. POST /ingest/presign with HMAC signature → get presigned S3 URL
187
+ Request includes: appId, eventId, contentHash, timestamp, nonce
188
+ 2. PUT snapshot bytes to presigned S3 URL
189
+
190
+ Delete:
191
+ 1. Build DELETE snapshot (action: 'DELETE', schema: null)
192
+ 2. POST /ingest/presign with HMAC signature → get presigned S3 URL
193
+ 3. PUT DELETE snapshot bytes to presigned S3 URL
194
+ ```
195
+
196
+ ## Snapshot Payload
197
+
198
+ The snapshot payload includes:
199
+
200
+ **Schema & Identity:**
201
+ - `schemaVersion` - Payload version for backward compatibility (current: 1.0)
202
+ - `.bprint` schema content (entity definitions, field types, constraints)
203
+ - Application ID and resource identifiers
204
+
205
+ **Infrastructure Metadata:**
206
+ - AWS account ID, region, stack information
207
+ - DynamoDB table configuration (keys, indexes, TTL, streams)
208
+ - CloudFormation context
209
+
210
+ **Versioning Strategy:**
211
+ - **Minor bump** (1.0 → 1.1): Additive or optional field changes
212
+ - **Major bump** (1.x → 2.0): Breaking changes (removed/renamed/required fields)
213
+
214
+ ## Configuration
215
+
216
+ ### Environment Configuration
217
+
218
+ The API defaults to production: `https://api.chaim.co`
219
+
220
+ Override for different environments via CDK context:
221
+
222
+ ```bash
223
+ # Production (default - no context needed)
224
+ cdk deploy
225
+
226
+ # Development
227
+ cdk deploy --context chaimApiBaseUrl=https://api.dev.chaim.co
228
+
229
+ # Beta
230
+ cdk deploy --context chaimApiBaseUrl=https://api.beta.chaim.co
231
+ ```
232
+
233
+ You can also set a custom API URL via environment variable in the Lambda:
234
+ - `CHAIM_API_BASE_URL` - Overrides the default at runtime
235
+
236
+ ## License
237
+
238
+ Apache-2.0
@@ -0,0 +1,144 @@
1
+ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
2
+ import { Construct } from 'constructs';
3
+ import { SchemaData } from '@chaim-tools/chaim-bprint-spec';
4
+ import { BaseBinderProps } from '../types/base-binder-props';
5
+ import { DataStoreMetadata } from '../types/data-store-metadata';
6
+ import { TableBindingConfig } from '../types/table-binding-config';
7
+ /**
8
+ * Abstract base class for all Chaim data store binders.
9
+ *
10
+ * Provides shared infrastructure:
11
+ * - Schema loading and validation
12
+ * - Snapshot payload construction
13
+ * - LOCAL snapshot writing during CDK synth (to OS cache)
14
+ * - Lambda-backed custom resource for S3 presigned upload + snapshot-ref
15
+ *
16
+ * Subclasses implement `extractMetadata()` for store-specific metadata extraction
17
+ * and optionally override `getTable()` for DynamoDB-like resources.
18
+ */
19
+ export declare abstract class BaseChaimBinder extends Construct {
20
+ /** Validated schema data */
21
+ readonly schemaData: SchemaData;
22
+ /** Extracted data store metadata */
23
+ readonly dataStoreMetadata: DataStoreMetadata;
24
+ /** Generated resource ID ({resourceName}__{entityName}[__N]) */
25
+ readonly resourceId: string;
26
+ /** Binding configuration */
27
+ readonly config: TableBindingConfig;
28
+ /** Base props (for internal use) */
29
+ protected readonly baseProps: BaseBinderProps;
30
+ constructor(scope: Construct, id: string, props: BaseBinderProps);
31
+ /**
32
+ * Validate that all bindings to the same table use the same config.
33
+ *
34
+ * This is a safety check - sharing the same TableBindingConfig object
35
+ * already ensures consistency, but this catches cases where users
36
+ * create separate configs with identical values.
37
+ */
38
+ private validateTableConsistency;
39
+ /**
40
+ * Abstract method - subclasses implement store-specific metadata extraction.
41
+ */
42
+ protected abstract extractMetadata(): DataStoreMetadata;
43
+ /**
44
+ * Override in subclasses to provide the table construct for stable identity.
45
+ * Default returns undefined (will fall back to construct path).
46
+ */
47
+ protected getTable(): dynamodb.ITable | undefined;
48
+ /**
49
+ * Get the resource name for display and filenames.
50
+ * For DynamoDB, this is the user label (not necessarily the physical table name).
51
+ *
52
+ * Subclasses can override this to provide a more meaningful name
53
+ * (e.g., construct node ID instead of physical resource name which may contain tokens).
54
+ */
55
+ protected getResourceName(): string;
56
+ /**
57
+ * Get the entity name from schema.
58
+ */
59
+ private getEntityName;
60
+ /**
61
+ * Read package version from package.json.
62
+ * Used to populate producer metadata in snapshots.
63
+ */
64
+ private getPackageVersion;
65
+ /**
66
+ * Build a LOCAL snapshot payload for CLI consumption.
67
+ * Does not include eventId or contentHash - those are generated at deploy-time.
68
+ */
69
+ private buildLocalSnapshot;
70
+ /**
71
+ * Build resource metadata from dataStore metadata.
72
+ */
73
+ private buildResourceMetadata;
74
+ /**
75
+ * Detect if metadata contains unresolved CDK tokens.
76
+ */
77
+ private detectUnresolvedTokens;
78
+ /**
79
+ * Generate a UUID v4 for operation tracking.
80
+ */
81
+ private generateUuid;
82
+ /**
83
+ * Apply snapshot cache policy based on CDK context.
84
+ *
85
+ * Checks the `chaimSnapshotCachePolicy` context value:
86
+ * - NONE (default): No cleanup
87
+ * - PRUNE_STACK: Delete existing stack snapshots before writing new ones
88
+ *
89
+ * This runs once per stack (tracked by static flag) to avoid
90
+ * multiple cleanup attempts when binding multiple entities.
91
+ */
92
+ private applySnapshotCachePolicy;
93
+ /**
94
+ * Parse snapshot cache policy from context value.
95
+ */
96
+ private parseSnapshotCachePolicy;
97
+ /**
98
+ * Extract stack name from stableResourceKey.
99
+ * Format: dynamodb:path:StackName/ResourceName
100
+ */
101
+ private extractStackNameFromResourceKey;
102
+ /**
103
+ * Write LOCAL snapshot to OS cache for chaim-cli consumption.
104
+ * Uses hierarchical path: aws/{accountId}/{region}/{stackName}/{datastoreType}/{resourceId}.json
105
+ *
106
+ * @param snapshot - The snapshot payload to write
107
+ * @returns The path where snapshot was written
108
+ */
109
+ private writeLocalSnapshotToDisk;
110
+ /**
111
+ * Find the CDK project root by walking up from current module.
112
+ */
113
+ private findCdkProjectRoot;
114
+ /**
115
+ * Write snapshot and Lambda handler to isolated CDK asset directory for Lambda bundling.
116
+ *
117
+ * Asset directory is per {stackName}/{resourceId} and MUST NOT be shared.
118
+ * The Lambda reads ./snapshot.json from its bundle, NOT from env vars or OS cache.
119
+ *
120
+ * The handler is copied from the canonical handler file (src/lambda-handler/handler.js)
121
+ * rather than being generated inline - this ensures a single source of truth.
122
+ *
123
+ * @returns The asset directory path
124
+ */
125
+ private writeSnapshotAsset;
126
+ /**
127
+ * Deploy Lambda function and custom resource for ingestion.
128
+ */
129
+ private deployIngestionResources;
130
+ /**
131
+ * Create Lambda function for ingestion workflow.
132
+ * Lambda reads snapshot from its bundled asset directory.
133
+ */
134
+ private createIngestionLambda;
135
+ /**
136
+ * Build Lambda environment variables.
137
+ * Note: Snapshot is NOT passed via env - Lambda reads from bundled asset.
138
+ */
139
+ private buildLambdaEnvironment;
140
+ /**
141
+ * Create CloudFormation custom resource.
142
+ */
143
+ private createCustomResource;
144
+ }