Skip to content

Commit ebd2de6

Browse files
committed
Release v1.8.0
1 parent 8b79593 commit ebd2de6

26 files changed

Lines changed: 3201 additions & 122 deletions

Dockerfile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,25 @@ COPY --chown=app:app src/syncer-attio/lib/go.mod src/syncer-attio/lib/go.sum /ap
4949
COPY --chown=app:app src/syncer-attio/go.mod src/syncer-attio/go.sum /app/src/syncer-attio/
5050
RUN cd /app/src/syncer-attio && go mod download
5151

52+
COPY --chown=app:app src/syncer-dialpad/lib/go.mod src/syncer-dialpad/lib/go.sum /app/src/syncer-dialpad/lib/
53+
COPY --chown=app:app src/syncer-dialpad/go.mod src/syncer-dialpad/go.sum /app/src/syncer-dialpad/
54+
RUN cd /app/src/syncer-dialpad && go mod download
55+
5256
COPY --chown=app:app src/server/go.mod src/server/go.sum /app/src/server/
5357
RUN cd /app/src/server && go mod download
5458

5559
COPY --chown=app:app src/common /app/src/common
5660
COPY --chown=app:app src/syncer-postgres /app/src/syncer-postgres
5761
COPY --chown=app:app src/syncer-amplitude /app/src/syncer-amplitude
5862
COPY --chown=app:app src/syncer-attio /app/src/syncer-attio
63+
COPY --chown=app:app src/syncer-dialpad /app/src/syncer-dialpad
5964
COPY --chown=app:app src/server /app/src/server
6065

6166
RUN ARCH=$(dpkg --print-architecture) \
6267
&& cd /app/src/syncer-postgres && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/bin/syncer-postgres \
6368
&& cd /app/src/syncer-amplitude && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/bin/syncer-amplitude \
6469
&& cd /app/src/syncer-attio && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/bin/syncer-attio \
70+
&& cd /app/src/syncer-dialpad && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/bin/syncer-dialpad \
6571
&& cd /app/src/server && CGO_ENABLED=1 GOOS=linux GOARCH=$ARCH go build -o /app/bin/server
6672

6773
# Prepare final image ##############################################################################
@@ -72,6 +78,7 @@ COPY --chown=app:app --from=compile \
7278
/app/bin/syncer-postgres \
7379
/app/bin/syncer-amplitude \
7480
/app/bin/syncer-attio \
81+
/app/bin/syncer-dialpad \
7582
/app/bin/server \
7683
/app/bin/
7784
COPY --chown=app:app docker/bin /app/bin/

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ install:
66
cd ../syncer-postgres && go mod tidy && cd ../lib && go mod tidy && \
77
cd ../../syncer-amplitude && go mod tidy && cd ./lib && go mod tidy && \
88
cd ../../syncer-attio && go mod tidy && cd ./lib && go mod tidy && \
9+
cd ../../syncer-dialpad && go mod tidy && cd ./lib && go mod tidy && \
910
cd ../../server && go mod tidy"
1011

1112
lint:
1213
devbox run "cd src/common && go fmt && staticcheck . && \
1314
cd ../syncer-postgres && go fmt && deadcode . && staticcheck . && cd ./lib && go fmt && staticcheck . && \
1415
cd ../../syncer-amplitude && go fmt && deadcode . && staticcheck . && cd ./lib && go fmt && staticcheck . && \
1516
cd ../../syncer-attio && go fmt && deadcode . && staticcheck . && cd ./lib && go fmt && staticcheck . && \
17+
cd ../../syncer-dialpad && go fmt && deadcode . && staticcheck . && cd ./lib && go fmt && staticcheck . && \
1618
cd ../../server && go fmt && deadcode . && staticcheck ."
1719

1820
build:

README.md

Lines changed: 121 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ BemiDB is an open-source Snowflake and Fivetran alternative bundled together. It
1010
- [Use cases](#use-cases)
1111
- [Quickstart](#quickstart)
1212
- [Usage](#usage)
13+
- [Syncing from Amplitude](#syncing-from-amplitude)
14+
- [Syncing from Attio](#syncing-from-attio)
15+
- [Syncing from Dialpad](#syncing-from-dialpad)
16+
- [Syncing from Postgres](#syncing-from-postgres)
17+
- [Customizing S3 endpoint](#customizing-s3-endpoint)
1318
- [Configuration](#configuration)
1419
- [Architecture](#architecture)
1520
- [Benchmark](#benchmark)
@@ -118,50 +123,124 @@ psql postgres://localhost:54321/bemidb -c "SELECT COUNT(*) FROM postgres.[table_
118123

119124
## Usage
120125

121-
#### Syncing from Postgres
126+
#### Syncing from Amplitude
122127

123-
By default, BemiDB syncs all tables from the Postgres database. To include and sync only specific tables from your Postgres database:
128+
1. Create an [Amplitude API key](https://docs.gettelio.com/integrations/amplitude)
129+
2. Run the syncer:
124130

125131
```sh
126132
docker run \
127-
-e SOURCE_POSTGRES_DATABASE_URL=postgres://user:password@host.docker.internal:5432/source \
128-
-e SOURCE_POSTGRES_INCLUDE_TABLES=public.table1,public.table2 \ # A comma-separated list of tables to include
129-
-e DESTINATION_SCHEMA_NAME=postgres \
133+
-e SOURCE_AMPLITUDE_API_KEY=[...] \
134+
-e SOURCE_AMPLITUDE_SECRET_KEY=[...] \
135+
-e SOURCE_AMPLITUDE_START_DATE=2025-01-01 \
136+
-e DESTINATION_SCHEMA_NAME=amplitude \
130137
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
131-
ghcr.io/bemihq/bemidb:latest syncer-postgres
138+
ghcr.io/bemihq/bemidb:latest syncer-amplitude
132139
```
133140

134-
To exclude specific tables during the sync:
141+
#### Syncing from Attio
142+
143+
1. Create an [Attio API access token](https://docs.gettelio.com/integrations/attio)
144+
2. Run the syncer:
135145

136146
```sh
137147
docker run \
138-
-e SOURCE_POSTGRES_DATABASE_URL=postgres://user:password@host.docker.internal:5432/source \
139-
-e SOURCE_POSTGRES_EXCLUDE_TABLES=public.audit_log,public.cache \ # A comma-separated list of tables to exclude
140-
-e DESTINATION_SCHEMA_NAME=postgres \
148+
-e SOURCE_ATTIO_API_ACCESS_TOKEN=[...] \
149+
-e DESTINATION_SCHEMA_NAME=attio \
141150
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
142-
ghcr.io/bemihq/bemidb:latest syncer-postgres
151+
ghcr.io/bemihq/bemidb:latest syncer-attio
143152
```
144153

145-
#### Syncing from Amplitude
154+
#### Syncing from Dialpad
155+
156+
1. Create a [Dialpad API key](https://docs.gettelio.com/integrations/dialpad)
157+
2. Create a webhook endpoint:
158+
159+
```sh
160+
curl -X POST "https://dialpad.com/api/v2/webhooks" \
161+
-H "Content-Type: application/json" \
162+
-H "Authorization: Bearer [DIALPAD_API_KEY]" \
163+
-d '{
164+
"hook_url": "https://[YOUR_DOMAIN]/[YOUR_WEBHOOK_ENDPOINT]",
165+
"secret": "[YOUR_WEBHOOK_SECRET]"
166+
}'
167+
```
168+
169+
3. Subscribe to SMS events for the created webhook:
170+
171+
```sh
172+
curl -X POST "https://dialpad.com/api/v2/subscriptions/sms" \
173+
-H "Content-Type: application/json" \
174+
-H "Authorization: Bearer [DIALPAD_API_KEY]" \
175+
-d '{
176+
"direction": "all",
177+
"enabled": true,
178+
"endpoint_id": "[WEBHOOK_ID]",
179+
"include_internal": false,
180+
"status": false
181+
}'
182+
```
183+
184+
4. Write a small service to receive Dialpad webhook events and publish them to NATS JetStream.
185+
186+
<details>
187+
<summary>See example code in Node.js</summary>
188+
189+
```ts
190+
import express from 'express';
191+
import bodyParser from 'body-parser';
192+
import { jwtVerify } from 'jose';
193+
import { connect, JSONCodec } from 'nats';
194+
195+
const app = express();
196+
app.use(bodyParser.json());
197+
app.post('/dialpad-webhook', async (req, res) => {
198+
const { payload } = await jwtVerify(request.body, new TextEncoder().encode('[YOUR_WEBHOOK_SECRET]'), { algorithms: ['HS256'] });
199+
const jsonCodec = JSONCodec();
200+
const natsConnection = await connect({ servers: "nats://host.docker.internal:4222" });
201+
const jetstreamManager = await natsConnection.jetstreamManager();
202+
await jetstreamManager.streams.add({ name: 'bemidb', subjects: ['bemidb.dialpad'] });
203+
await jetstreamManager.jetstream().publish('bemidb.dialpad', jsonCodec.encode(payload));
204+
});
205+
app.listen(3000, () => console.log('Server is running on port 3000'));
206+
```
207+
</details>
208+
209+
5. Run the syncer:
146210

147211
```sh
148212
docker run \
149-
-e SOURCE_AMPLITUDE_API_KEY=[...] \
150-
-e SOURCE_AMPLITUDE_SECRET_KEY=[...] \
151-
-e SOURCE_AMPLITUDE_START_DATE=2025-01-01 \
152-
-e DESTINATION_SCHEMA_NAME=amplitude \
213+
-e NATS_URL=nats://host.docker.internal:4222 \
214+
-e NATS_JETSTREAM_STREAM=bemidb \
215+
-e NATS_JETSTREAM_SUBJECT=bemidb.dialpad \
216+
-e NATS_JETSTREAM_CONSUMER_NAME=bemidb-dialpad \
217+
-e DESTINATION_SCHEMA_NAME=dialpad \
153218
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
154-
ghcr.io/bemihq/bemidb:latest syncer-amplitude
219+
ghcr.io/bemihq/bemidb:latest syncer-dialpad
155220
```
156221

157-
#### Syncing from Attio
222+
#### Syncing from Postgres
223+
224+
By default, BemiDB syncs all tables from the Postgres database. To include and sync only specific tables from your Postgres database:
158225

159226
```sh
160227
docker run \
161-
-e SOURCE_ATTIO_API_ACCESS_TOKEN=[...] \
162-
-e DESTINATION_SCHEMA_NAME=attio \
228+
-e SOURCE_POSTGRES_DATABASE_URL=postgres://user:password@host.docker.internal:5432/source \
229+
-e SOURCE_POSTGRES_INCLUDE_TABLES=public.table1,public.table2 \ # A comma-separated list of tables to include
230+
-e DESTINATION_SCHEMA_NAME=postgres \
163231
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
164-
ghcr.io/bemihq/bemidb:latest syncer-attio
232+
ghcr.io/bemihq/bemidb:latest syncer-postgres
233+
```
234+
235+
To exclude specific tables during the sync:
236+
237+
```sh
238+
docker run \
239+
-e SOURCE_POSTGRES_DATABASE_URL=postgres://user:password@host.docker.internal:5432/source \
240+
-e SOURCE_POSTGRES_EXCLUDE_TABLES=public.audit_log,public.cache \ # A comma-separated list of tables to exclude
241+
-e DESTINATION_SCHEMA_NAME=postgres \
242+
-e AWS_REGION -e AWS_S3_BUCKET -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e CATALOG_DATABASE_URL \
243+
ghcr.io/bemihq/bemidb:latest syncer-postgres
165244
```
166245

167246
#### Customizing S3 endpoint
@@ -195,15 +274,6 @@ export AWS_S3_ENDPOINT=http://localhost:9000
195274

196275
## Configuration
197276

198-
#### `syncer-postgres` command options
199-
200-
| Environment variable | Default value | Description |
201-
|-------------------------------------|---------------|----------------------------------------------------------------------|
202-
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
203-
| `SOURCE_POSTGRES_DATABASE_URL` | Required | Postgres database URL to sync data from. |
204-
| `SOURCE_POSTGRES_INCLUDE_TABLES` | | List of tables to include in sync. Comma-separated `schema.table`. |
205-
| `SOURCE_POSTGRES_EXCLUDE_TABLES` | | List of tables to exclude from sync. Comma-separated `schema.table`. |
206-
207277
#### `syncer-amplitude` command options
208278

209279
| Environment variable | Default value | Description |
@@ -220,6 +290,26 @@ export AWS_S3_ENDPOINT=http://localhost:9000
220290
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
221291
| `SOURCE_ATTIO_API_ACCESS_TOKEN` | Required | Attio API access token for authentication. |
222292

293+
#### `syncer-dialpad` command options
294+
295+
| Environment variable | Default value | Description |
296+
|--------------------------------|---------------|----------------------------------------------------------------|
297+
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
298+
| `NATS_URL` | Required | NATS server URL for connecting to receive Dialpad SMS records. |
299+
| `NATS_JETSTREAM_STREAM` | Required | NATS JetStream stream name. |
300+
| `NATS_JETSTREAM_SUBJECT` | Required | NATS JetStream subject name. |
301+
| `NATS_JETSTREAM_CONSUMER_NAME` | Required | NATS JetStream consumer name. |
302+
| `NATS_FETCH_TIMEOUT_SECONDS` | `30` | Timeout in seconds for fetching messages from NATS. |
303+
304+
#### `syncer-postgres` command options
305+
306+
| Environment variable | Default value | Description |
307+
|-------------------------------------|---------------|----------------------------------------------------------------------|
308+
| `DESTINATION_SCHEMA_NAME` | Required | Schema name in BemiDB to sync data to. |
309+
| `SOURCE_POSTGRES_DATABASE_URL` | Required | Postgres database URL to sync data from. |
310+
| `SOURCE_POSTGRES_INCLUDE_TABLES` | | List of tables to include in sync. Comma-separated `schema.table`. |
311+
| `SOURCE_POSTGRES_EXCLUDE_TABLES` | | List of tables to exclude from sync. Comma-separated `schema.table`. |
312+
223313
#### `server` command options
224314

225315
| Environment variable | Default value | Description |
@@ -323,6 +413,7 @@ SELECT * FROM [TABLE] WHERE [JSON_COLUMN]->>'[JSON_KEY]' = '[JSON_VALUE]';
323413
- [x] Amplitude (incremental)
324414
- [x] Attio CRM (full-refresh)
325415
- [x] Postgres (full-refresh)
416+
- [x] Dialpad (real-time)
326417
- [ ] HubSpot
327418
- [ ] Stripe
328419
- [ ] Google Sheets

docker/bin/run.sh

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ case "${1:-}" in
3737
./bin/syncer-attio 2>&1 | sed 's/^/[Syncer] /'
3838
echo "Syncer for Attio finished."
3939
;;
40+
syncer-dialpad)
41+
: "${NATS_URL:?Environment variable NATS_URL must be set}"
42+
: "${NATS_JETSTREAM_STREAM:?Environment variable NATS_JETSTREAM_STREAM must be set}"
43+
: "${NATS_JETSTREAM_SUBJECT:?Environment variable NATS_JETSTREAM_SUBJECT must be set}"
44+
: "${NATS_JETSTREAM_CONSUMER_NAME:?Environment variable NATS_JETSTREAM_CONSUMER_NAME must be set}"
45+
: "${DESTINATION_SCHEMA_NAME:?Environment variable DESTINATION_SCHEMA_NAME must be set}"
46+
47+
psql $CATALOG_DATABASE_URL -f /app/scripts/catalog.sql
48+
49+
echo "Starting Syncer for Dialpad..."
50+
./bin/syncer-dialpad 2>&1 | sed 's/^/[Syncer] /'
51+
echo "Syncer for Dialpad finished."
52+
;;
4053
server)
4154
: "${AWS_REGION:?Environment variable AWS_REGION must be set}"
4255
: "${AWS_S3_BUCKET:?Environment variable AWS_S3_BUCKET must be set}"

src/common/duckdb_client.go

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,52 +14,46 @@ var SYNCER_DUCKDB_BOOT_QUERIES = []string{
1414
}
1515

1616
type DuckdbClient struct {
17-
Config *CommonConfig
18-
Db *sql.DB
19-
Connector *duckdb.Connector
17+
Config *CommonConfig
18+
Db *sql.DB
19+
Connector *duckdb.Connector
20+
BootQueries []string
2021
}
2122

2223
func NewDuckdbClient(config *CommonConfig, bootQueries ...[]string) *DuckdbClient {
2324
ctx := context.Background()
2425
connector, err := duckdb.NewConnector("", nil)
2526
PanicIfError(config, err)
2627
db := sql.OpenDB(connector)
27-
PanicIfError(config, err)
2828

2929
client := &DuckdbClient{
3030
Config: config,
3131
Db: db,
3232
Connector: connector,
3333
}
3434

35-
queries := []string{
35+
client.BootQueries = []string{
3636
"SET timezone='UTC'",
3737
}
3838
if bootQueries != nil {
39-
queries = append(queries, bootQueries[0]...)
40-
}
41-
for _, query := range queries {
42-
_, err := client.ExecContext(ctx, query)
43-
PanicIfError(config, err)
39+
client.BootQueries = append(client.BootQueries, bootQueries[0]...)
4440
}
45-
46-
client.setExplicitAwsCredentials(ctx)
47-
41+
client.BootQueries = append(
42+
client.BootQueries,
43+
"CREATE OR REPLACE SECRET aws_s3_secret (TYPE S3, KEY_ID '"+config.Aws.AccessKeyId+"', SECRET '"+config.Aws.SecretAccessKey+"', REGION '"+config.Aws.Region+"', ENDPOINT '"+config.Aws.S3Endpoint+"', SCOPE 's3://"+config.Aws.S3Bucket+"')",
44+
)
4845
if IsLocalHost(config.Aws.S3Endpoint) {
49-
_, err = client.ExecContext(ctx, "SET s3_use_ssl=false")
50-
PanicIfError(config, err)
46+
client.BootQueries = append(client.BootQueries, "SET s3_use_ssl=false")
5147
}
52-
5348
if config.Aws.S3Endpoint != DEFAULT_AWS_S3_ENDPOINT {
54-
// Use endpoint/bucket/key (path, deprecated on AWS) instead of bucket.endpoint/key (vhost)
55-
_, err = client.ExecContext(ctx, "SET s3_url_style='path'")
56-
PanicIfError(config, err)
49+
client.BootQueries = append(client.BootQueries, "SET s3_url_style='path'") // Use endpoint/bucket/key (path, deprecated on AWS) instead of bucket.endpoint/key (vhost)
5750
}
58-
5951
if config.LogLevel == LOG_LEVEL_TRACE {
60-
_, err = client.ExecContext(ctx, "PRAGMA enable_logging('HTTP')")
61-
PanicIfError(config, err)
62-
_, err = client.ExecContext(ctx, "SET logging_storage = 'stdout'")
52+
client.BootQueries = append(client.BootQueries, "PRAGMA enable_logging('HTTP')", "SET logging_storage = 'stdout'")
53+
}
54+
55+
for _, query := range client.BootQueries {
56+
_, err := client.ExecContext(ctx, query)
6357
PanicIfError(config, err)
6458
}
6559

@@ -130,17 +124,22 @@ func (client *DuckdbClient) Close() {
130124
client.Db.Close()
131125
}
132126

133-
func (client *DuckdbClient) setExplicitAwsCredentials(ctx context.Context) {
134-
config := client.Config
135-
query := "CREATE OR REPLACE SECRET aws_s3_secret (TYPE S3, KEY_ID '$accessKeyId', SECRET '$secretAccessKey', REGION '$region', ENDPOINT '$endpoint', SCOPE '$s3Bucket')"
136-
_, err := client.ExecContext(ctx, query, map[string]string{
137-
"accessKeyId": config.Aws.AccessKeyId,
138-
"secretAccessKey": config.Aws.SecretAccessKey,
139-
"region": config.Aws.Region,
140-
"endpoint": config.Aws.S3Endpoint,
141-
"s3Bucket": "s3://" + config.Aws.S3Bucket,
142-
})
143-
PanicIfError(config, err)
127+
func (client *DuckdbClient) RecreateDb() {
128+
ctx := context.Background()
129+
130+
client.Db.Close()
131+
132+
connector, err := duckdb.NewConnector("", nil)
133+
PanicIfError(client.Config, err)
134+
db := sql.OpenDB(connector)
135+
client.Db = db
136+
client.Connector = connector
137+
138+
for _, query := range client.BootQueries {
139+
_, err := client.Db.ExecContext(ctx, query)
140+
PanicIfError(client.Config, err)
141+
}
142+
144143
}
145144

146145
func replaceNamedStringArgs(query string, args map[string]string) string {

0 commit comments

Comments
 (0)