Skip to content

Commit da678cf

Browse files
tonyxiaoclaude
andcommitted
Add AWS DSQL destination connector with Terraform and demo
- New `packages/destination-aws-dsql` connector using IAM auth via `@aws-sdk/dsql-signer`, adapted for DSQL limitations (no jsonb, no triggers, no generated columns, sequential DDL) - Terraform config in `terraform/` to provision a DSQL cluster - `demo/stripe-to-dsql.ts` syncs Stripe data to DSQL with built-in verification queries, auto-reads endpoint from terraform output - Registered `aws_dsql` in engine default connectors Verified: 1000 customers, 720 prices, 702 products synced successfully. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Committed-By-Agent: claude
1 parent 220b601 commit da678cf

13 files changed

Lines changed: 1197 additions & 11 deletions

File tree

apps/engine/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@hono/node-server": "^1",
4545
"@scalar/hono-api-reference": "^0.6",
4646
"@stripe/sync-destination-google-sheets": "workspace:*",
47+
"@stripe/sync-destination-aws-dsql": "workspace:*",
4748
"@stripe/sync-destination-postgres": "workspace:*",
4849
"@stripe/sync-hono-zod-openapi": "workspace:*",
4950
"@stripe/sync-integration-supabase": "workspace:*",

apps/engine/src/lib/default-connectors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sourceStripe from '@stripe/sync-source-stripe'
2+
import destinationAwsDsql from '@stripe/sync-destination-aws-dsql'
23
import destinationPostgres from '@stripe/sync-destination-postgres'
34
import destinationGoogleSheets from '@stripe/sync-destination-google-sheets'
45
import type { RegisteredConnectors } from './resolver.js'
@@ -7,6 +8,7 @@ import type { RegisteredConnectors } from './resolver.js'
78
export const defaultConnectors: RegisteredConnectors = {
89
sources: { stripe: sourceStripe },
910
destinations: {
11+
aws_dsql: destinationAwsDsql,
1012
postgres: destinationPostgres,
1113
google_sheets: destinationGoogleSheets,
1214
},

demo/README.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,28 @@ This syncs `products`, `prices`, and `customers` into Postgres with full schema
9696

9797
## All demos
9898

99-
| Script | What it does | Required env vars |
100-
|--------|-------------|-------------------|
101-
| `read-from-stripe.sh` | Read from Stripe, output NDJSON to stdout | `STRIPE_API_KEY` |
102-
| `write-to-postgres.sh` | Write NDJSON (stdin or sample data) to Postgres | `DATABASE_URL` |
103-
| `write-to-sheets.sh` | Write NDJSON (stdin or sample data) to Google Sheets | `GOOGLE_*` |
104-
| `stripe-to-postgres.sh` | Stripe → Postgres via the engine | `STRIPE_API_KEY`, `DATABASE_URL` |
105-
| `stripe-to-google-sheets.sh` | Stripe → Google Sheets via the engine | `STRIPE_API_KEY`, `GOOGLE_*` |
99+
| Script | What it does | Required env vars |
100+
| ---------------------------- | ---------------------------------------------------- | -------------------------------- |
101+
| `read-from-stripe.sh` | Read from Stripe, output NDJSON to stdout | `STRIPE_API_KEY` |
102+
| `write-to-postgres.sh` | Write NDJSON (stdin or sample data) to Postgres | `DATABASE_URL` |
103+
| `write-to-sheets.sh` | Write NDJSON (stdin or sample data) to Google Sheets | `GOOGLE_*` |
104+
| `stripe-to-postgres.sh` | Stripe → Postgres via the engine | `STRIPE_API_KEY`, `DATABASE_URL` |
105+
| `stripe-to-google-sheets.sh` | Stripe → Google Sheets via the engine | `STRIPE_API_KEY`, `GOOGLE_*` |
106+
| `stripe-to-dsql.ts` | Stripe → AWS DSQL via the engine | `STRIPE_API_KEY`, `AWS_*` |
107+
108+
### Stripe → AWS DSQL
109+
110+
Sync Stripe data to [Aurora DSQL](https://aws.amazon.com/rds/aurora/dsql/) (serverless distributed SQL):
111+
112+
```sh
113+
# One-time: provision the DSQL cluster
114+
cd terraform && terraform init && terraform apply && cd ..
115+
116+
# Sync (auto-reads endpoint from terraform output)
117+
node --import tsx demo/stripe-to-dsql.ts
118+
```
119+
120+
Or with explicit env vars: `DSQL_ENDPOINT=<id>.dsql.<region>.on.aws node --import tsx demo/stripe-to-dsql.ts`
106121

107122
### TypeScript API
108123

@@ -115,7 +130,7 @@ node --import tsx demo/stripe-to-google-sheets.ts
115130

116131
## Utilities
117132

118-
| Script | What it does |
119-
|--------|-------------|
120-
| `reset-postgres.sh` | Drop all tables and non-system schemas |
121-
| `webhooksite.sh` | Set up webhook forwarding for live Stripe events |
133+
| Script | What it does |
134+
| ------------------- | ------------------------------------------------ |
135+
| `reset-postgres.sh` | Drop all tables and non-system schemas |
136+
| `webhooksite.sh` | Set up webhook forwarding for live Stripe events |

demo/stripe-to-dsql.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Sync Stripe → AWS DSQL via the engine API (TypeScript).
3+
*
4+
* Usage:
5+
* npx tsx demo/stripe-to-dsql.ts
6+
* bun demo/stripe-to-dsql.ts
7+
*
8+
* Env:
9+
* STRIPE_API_KEY — Stripe secret key
10+
* DSQL_ENDPOINT — DSQL cluster endpoint (e.g. <id>.dsql.us-east-1.on.aws)
11+
* AWS_REGION — AWS region (default: us-east-1)
12+
* AWS_ACCESS_KEY_ID — AWS credentials
13+
* AWS_SECRET_ACCESS_KEY — AWS credentials
14+
*/
15+
import { execSync } from 'node:child_process'
16+
import { createConnectorResolver, createEngine } from '../apps/engine/src/lib/index.js'
17+
import { defaultConnectors } from '../apps/engine/src/lib/default-connectors.js'
18+
import { fileStateStore } from '../apps/engine/src/lib/state-store.js'
19+
import type { PipelineConfig } from '../packages/protocol/src/index.js'
20+
import { buildPoolConfig, pg } from '../packages/destination-aws-dsql/src/index.js'
21+
22+
const stripeApiKey = process.env.STRIPE_API_KEY
23+
const region = process.env.AWS_REGION ?? 'us-east-1'
24+
25+
// Auto-read endpoint from terraform output if not set
26+
const dsqlEndpoint =
27+
process.env.DSQL_ENDPOINT ??
28+
(() => {
29+
try {
30+
return execSync('terraform -chdir=terraform output -raw cluster_endpoint', {
31+
encoding: 'utf8',
32+
}).trim()
33+
} catch {
34+
return undefined
35+
}
36+
})()
37+
38+
if (!stripeApiKey) throw new Error('Set STRIPE_API_KEY')
39+
if (!dsqlEndpoint)
40+
throw new Error('Set DSQL_ENDPOINT or run `terraform -chdir=terraform apply` first')
41+
42+
const pipeline: PipelineConfig = {
43+
source: { type: 'stripe', stripe: { api_key: stripeApiKey, backfill_limit: 10 } },
44+
destination: {
45+
type: 'aws_dsql',
46+
aws_dsql: { endpoint: dsqlEndpoint, region, schema: 'public' },
47+
},
48+
streams: [{ name: 'products' }, { name: 'prices' }, { name: 'customers' }],
49+
}
50+
51+
const resolver = await createConnectorResolver(defaultConnectors, { path: true })
52+
const engine = await createEngine(resolver)
53+
54+
// Create tables
55+
for await (const _msg of engine.pipeline_setup(pipeline)) {
56+
}
57+
58+
// State: file-backed, resumable across runs
59+
const store = fileStateStore('.sync-state-dsql.json')
60+
const state = await store.get()
61+
62+
// Sync
63+
for await (const msg of engine.pipeline_sync(pipeline, { state })) {
64+
if (msg.type === 'source_state') {
65+
if (msg.source_state.state_type === 'global') await store.setGlobal(msg.source_state.data)
66+
else await store.set(msg.source_state.stream, msg.source_state.data)
67+
}
68+
console.log(JSON.stringify(msg))
69+
}
70+
71+
// Verify: query DSQL to show what was synced
72+
console.log('\n--- Verifying data in DSQL ---')
73+
const poolConfig = await buildPoolConfig({
74+
endpoint: dsqlEndpoint,
75+
region,
76+
schema: 'public',
77+
batch_size: 100,
78+
})
79+
const pool = new pg.Pool(poolConfig)
80+
81+
for (const table of ['customers', 'prices', 'products']) {
82+
const { rows } = await pool.query(`SELECT count(*) FROM ${table}`)
83+
console.log(`${table}: ${rows[0].count} rows`)
84+
}
85+
86+
console.log('\nSample rows:')
87+
for (const table of ['customers', 'products']) {
88+
const { rows } = await pool.query(
89+
`SELECT id, substring(_raw_data, 1, 100) as data FROM ${table} LIMIT 2`
90+
)
91+
for (const row of rows) console.log(` [${table}] ${row.id}: ${row.data}...`)
92+
}
93+
94+
await pool.end()
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@stripe/sync-destination-aws-dsql",
3+
"version": "0.1.0",
4+
"private": false,
5+
"type": "module",
6+
"exports": {
7+
".": {
8+
"bun": "./src/index.ts",
9+
"types": "./dist/index.d.ts",
10+
"import": "./dist/index.js"
11+
}
12+
},
13+
"scripts": {
14+
"build": "tsc",
15+
"test": "vitest"
16+
},
17+
"files": [
18+
"dist",
19+
"src"
20+
],
21+
"dependencies": {
22+
"@aws-sdk/dsql-signer": "^3.1013.0",
23+
"@stripe/sync-protocol": "workspace:*",
24+
"@stripe/sync-util-postgres": "workspace:*",
25+
"pg": "^8.16.3",
26+
"zod": "^4.3.6"
27+
},
28+
"devDependencies": {
29+
"@types/pg": "^8.15.5",
30+
"vitest": "^3.2.4"
31+
}
32+
}

0 commit comments

Comments
 (0)