Skip to content

Commit 1a0aada

Browse files
committed
✨ Postgres sink
1 parent c9b03c3 commit 1a0aada

File tree

26 files changed

+979
-28
lines changed

26 files changed

+979
-28
lines changed

assets/svelte/consumers/ShowSink.svelte

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
ElasticsearchConsumer,
2828
RedisStringConsumer,
2929
AzureEventHubConsumer,
30+
PostgresSinkConsumer,
3031
} from "./types";
3132
import AzureEventHubSinkCard from "../sinks/azure_event_hub/AzureEventHubSinkCard.svelte";
3233
import ElasticsearchSinkCard from "../sinks/elasticsearch/ElasticsearchSinkCard.svelte";
@@ -48,6 +49,8 @@
4849
import HealthAlerts from "$lib/health/HealthAlerts.svelte";
4950
import { Button } from "$lib/components/ui/button";
5051
import CollapsibleCode from "../components/CollapsibleCode.svelte";
52+
import PostgresSinkCard from "../sinks/postgres/PostgresSinkCard.svelte";
53+
import type { Table } from "../databases/types";
5154
5255
export let live;
5356
export let parent;
@@ -161,6 +164,12 @@
161164
return consumer.sink.type === "rabbitmq";
162165
}
163166
167+
function isPostgresSinkConsumer(
168+
consumer: Consumer,
169+
): consumer is PostgresSinkConsumer {
170+
return consumer.sink.type === "postgres";
171+
}
172+
164173
let chartElement;
165174
let updateChart;
166175
let resizeObserver;
@@ -761,6 +770,15 @@
761770
unit: units[unitIndex],
762771
};
763772
}
773+
774+
function getRoutingCode(consumer: Consumer): string | null {
775+
if (!consumer.routing || !consumer.routing.function) return null;
776+
const func = consumer.routing.function;
777+
if (func.type === "routing" && "code" in func) {
778+
return func.code;
779+
}
780+
return null;
781+
}
764782
</script>
765783

766784
<div class="flex flex-col flex-1">
@@ -1254,6 +1272,8 @@
12541272
<ElasticsearchSinkCard {consumer} />
12551273
{:else if isRedisStringConsumer(consumer)}
12561274
<RedisStringSinkCard {consumer} />
1275+
{:else if isPostgresSinkConsumer(consumer)}
1276+
<PostgresSinkCard {consumer} />
12571277
{/if}
12581278

12591279
<ShowSource {consumer} {tables} />

assets/svelte/consumers/ShowSinkHeader.svelte

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import AzureEventHubIcon from "../sinks/azure_event_hub/AzureEventHubIcon.svelte";
3333
import TypesenseIcon from "../sinks/typesense/TypesenseIcon.svelte";
3434
import ElasticsearchIcon from "../sinks/elasticsearch/ElasticsearchIcon.svelte";
35+
import PostgresIcon from "../sinks/postgres/PostgresIcon.svelte";
3536
import StopSinkModal from "./StopSinkModal.svelte";
3637
import { Badge } from "$lib/components/ui/badge";
3738
@@ -186,6 +187,8 @@
186187
<KinesisIcon class="h-6 w-6 mr-2" />
187188
{:else if consumer.sink.type === "s2"}
188189
<S2Icon class="h-6 w-6 mr-2" />
190+
{:else if consumer.sink.type === "postgres"}
191+
<PostgresIcon class="h-6 w-6 mr-2" />
189192
{/if}
190193
<h1 class="text-xl font-semibold">
191194
{consumer.name}

assets/svelte/consumers/SinkConsumerForm.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import TypesenseSinkForm from "$lib/sinks/typesense/TypesenseSinkForm.svelte";
3737
import MeilisearchSinkForm from "$lib/sinks/meilisearch/MeilisearchSinkForm.svelte";
3838
import ElasticsearchSinkForm from "$lib/sinks/elasticsearch/ElasticsearchSinkForm.svelte";
39+
import PostgresSinkForm from "$lib/sinks/postgres/PostgresSinkForm.svelte";
3940
import * as Alert from "$lib/components/ui/alert/index.js";
4041
import SchemaTableSelector from "../components/SchemaTableSelector.svelte";
4142
import * as Tooltip from "$lib/components/ui/tooltip";
@@ -731,6 +732,14 @@
731732
{functions}
732733
{functionRefreshState}
733734
/>
735+
{:else if consumer.type === "postgres"}
736+
<PostgresSinkForm
737+
errors={errors.consumer}
738+
bind:form
739+
{functions}
740+
{refreshFunctions}
741+
bind:functionRefreshState
742+
/>
734743
{:else if consumer.type === "kafka"}
735744
<KafkaSinkForm
736745
errors={errors.consumer}

assets/svelte/consumers/SinkIndex.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
import SequinStreamIcon from "../sinks/sequin_stream/SequinStreamIcon.svelte";
3030
import NatsIcon from "../sinks/nats/NatsIcon.svelte";
3131
import RabbitMqIcon from "../sinks/rabbitmq/RabbitMqIcon.svelte";
32-
3332
import TypesenseIcon from "../sinks/typesense/TypesenseIcon.svelte";
3433
import MeilisearchIcon from "../sinks/meilisearch/MeilisearchIcon.svelte";
3534
import ElasticsearchIcon from "../sinks/elasticsearch/ElasticsearchIcon.svelte";
35+
import PostgresIcon from "../sinks/postgres/PostgresIcon.svelte";
3636
3737
import { Badge } from "$lib/components/ui/badge";
3838
import * as d3 from "d3";
@@ -56,7 +56,8 @@
5656
| "nats"
5757
| "rabbitmq"
5858
| "typesense"
59-
| "elasticsearch";
59+
| "elasticsearch"
60+
| "postgres";
6061
6162
status: "active" | "disabled" | "paused";
6263
database_name: string;
@@ -164,6 +165,11 @@
164165
name: "Elasticsearch",
165166
icon: ElasticsearchIcon,
166167
},
168+
{
169+
id: "postgres",
170+
name: "Postgres",
171+
icon: PostgresIcon,
172+
},
167173
];
168174
169175
function handleConsumerClick(id: string, type: string) {

assets/svelte/consumers/dynamicRoutingDocs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,14 @@ export const routedSinkDocs: Record<RoutedSinkType, RoutedSinkDocs> = {
231231
},
232232
},
233233
},
234+
postgres: {
235+
fields: {
236+
table_name: {
237+
description: "Postgres table name",
238+
staticValue: "<empty>",
239+
staticFormField: "table_name",
240+
dynamicDefault: "<empty>",
241+
},
242+
},
243+
},
234244
};

assets/svelte/consumers/types.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export type BaseConsumer = {
3333
message_grouping: boolean;
3434
ack_wait_ms: number;
3535
max_ack_pending: number;
36+
max_retry_count: number;
3637
max_deliver: number;
3738
max_waiting: number;
3839
inserted_at: string;
@@ -247,6 +248,20 @@ export type ElasticsearchConsumer = BaseConsumer & {
247248
};
248249
};
249250

251+
// PostgreSQL specific sink
252+
export type PostgresSinkConsumer = BaseConsumer & {
253+
sink: {
254+
type: "postgres";
255+
host: string;
256+
port: number;
257+
database: string;
258+
table_name: string;
259+
username: string;
260+
ssl: boolean;
261+
routing_mode: "dynamic" | "static";
262+
};
263+
};
264+
250265
// Union type for all consumer types
251266
export type Consumer =
252267
| HttpPushConsumer
@@ -263,7 +278,8 @@ export type Consumer =
263278
| TypesenseConsumer
264279
| SnsConsumer
265280
| ElasticsearchConsumer
266-
| RedisStringConsumer;
281+
| RedisStringConsumer
282+
| PostgresSinkConsumer;
267283

268284
export const SinkTypeValues = [
269285
"http_push",
@@ -281,6 +297,7 @@ export const SinkTypeValues = [
281297
"meilisearch",
282298
"elasticsearch",
283299
"redis_string",
300+
"postgres",
284301
] as const;
285302

286303
export type SinkType = (typeof SinkTypeValues)[number];
@@ -301,6 +318,7 @@ export const RoutedSinkTypeValues = [
301318
"sqs",
302319
"sns",
303320
"kinesis",
321+
"postgres",
304322
] as const;
305323

306324
export type RoutedSinkType = (typeof RoutedSinkTypeValues)[number];
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<svg
2+
width="432.071pt"
3+
height="445.383pt"
4+
viewBox="0 0 432.071 445.383"
5+
xml:space="preserve"
6+
xmlns="http://www.w3.org/2000/svg"
7+
class={$$props.class}
8+
>
9+
<g
10+
id="orginal"
11+
style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;"
12+
>
13+
</g>
14+
<g
15+
id="Layer_x0020_3"
16+
style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#FFFFFF;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;"
17+
>
18+
<path
19+
style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;"
20+
d="M323.205,324.227c2.833-23.601,1.984-27.062,19.563-23.239l4.463,0.392c13.517,0.615,31.199-2.174,41.587-7c22.362-10.376,35.622-27.7,13.572-23.148c-50.297,10.376-53.755-6.655-53.755-6.655c53.111-78.803,75.313-178.836,56.149-203.322 C352.514-5.534,262.036,26.049,260.522,26.869l-0.482,0.089c-9.938-2.062-21.06-3.294-33.554-3.496c-22.761-0.374-40.032,5.967-53.133,15.904c0,0-161.408-66.498-153.899,83.628c1.597,31.936,45.777,241.655,98.47,178.31 c19.259-23.163,37.871-42.748,37.871-42.748c9.242,6.14,20.307,9.272,31.912,8.147l0.897-0.765c-0.281,2.876-0.157,5.689,0.359,9.019c-13.572,15.167-9.584,17.83-36.723,23.416c-27.457,5.659-11.326,15.734-0.797,18.367c12.768,3.193,42.305,7.716,62.268-20.224 l-0.795,3.188c5.325,4.26,4.965,30.619,5.72,49.452c0.756,18.834,2.017,36.409,5.856,46.771c3.839,10.36,8.369,37.05,44.036,29.406c29.809-6.388,52.6-15.582,54.677-101.107"
21+
/>
22+
<path
23+
style="fill:#336791;stroke:none;"
24+
d="M402.395,271.23c-50.302,10.376-53.76-6.655-53.76-6.655c53.111-78.808,75.313-178.843,56.153-203.326c-52.27-66.785-142.752-35.2-144.262-34.38l-0.486,0.087c-9.938-2.063-21.06-3.292-33.56-3.496c-22.761-0.373-40.026,5.967-53.127,15.902 c0,0-161.411-66.495-153.904,83.63c1.597,31.938,45.776,241.657,98.471,178.312c19.26-23.163,37.869-42.748,37.869-42.748c9.243,6.14,20.308,9.272,31.908,8.147l0.901-0.765c-0.28,2.876-0.152,5.689,0.361,9.019c-13.575,15.167-9.586,17.83-36.723,23.416 c-27.459,5.659-11.328,15.734-0.796,18.367c12.768,3.193,42.307,7.716,62.266-20.224l-0.796,3.188c5.319,4.26,9.054,27.711,8.428,48.969c-0.626,21.259-1.044,35.854,3.147,47.254c4.191,11.4,8.368,37.05,44.042,29.406c29.809-6.388,45.256-22.942,47.405-50.555 c1.525-19.631,4.976-16.729,5.194-34.28l2.768-8.309c3.192-26.611,0.507-35.196,18.872-31.203l4.463,0.392c13.517,0.615,31.208-2.174,41.591-7c22.358-10.376,35.618-27.7,13.573-23.148z"
25+
/>
26+
<path
27+
d="M215.866,286.484c-1.385,49.516,0.348,99.377,5.193,111.495c4.848,12.118,15.223,35.688,50.9,28.045c29.806-6.39,40.651-18.756,45.357-46.051c3.466-20.082,10.148-75.854,11.005-87.281"
28+
/>
29+
<path
30+
d="M173.104,38.256c0,0-161.521-66.016-154.012,84.109c1.597,31.938,45.779,241.664,98.473,178.316c19.256-23.166,36.671-41.335,36.671-41.335"
31+
/>
32+
<path
33+
d="M260.349,26.207c-5.591,1.753,89.848-34.889,144.087,34.417c19.159,24.484-3.043,124.519-56.153,203.329"
34+
/>
35+
<path
36+
style="stroke-linejoin:bevel;"
37+
d="M348.282,263.953c0,0,3.461,17.036,53.764,6.653c22.04-4.552,8.776,12.774-13.577,23.155c-18.345,8.514-59.474,10.696-60.146-1.069c-1.729-30.355,21.647-21.133,19.96-28.739c-1.525-6.85-11.979-13.573-18.894-30.338 c-6.037-14.633-82.796-126.849,21.287-110.183c3.813-0.789-27.146-99.002-124.553-100.599c-97.385-1.597-94.19,119.762-94.19,119.762"
38+
/>
39+
<path
40+
d="M188.604,274.334c-13.577,15.166-9.584,17.829-36.723,23.417c-27.459,5.66-11.326,15.733-0.797,18.365c12.768,3.195,42.307,7.718,62.266-20.229c6.078-8.509-0.036-22.086-8.385-25.547c-4.034-1.671-9.428-3.765-16.361,3.994z"
41+
/>
42+
<path
43+
d="M187.715,274.069c-1.368-8.917,2.93-19.528,7.536-31.942c6.922-18.626,22.893-37.255,10.117-96.339c-9.523-44.029-73.396-9.163-73.436-3.193c-0.039,5.968,2.889,30.26-1.067,58.548c-5.162,36.913,23.488,68.132,56.479,64.938"
44+
/>
45+
<path
46+
style="fill:#FFFFFF;stroke-width:4.155;stroke-linecap:butt;stroke-linejoin:miter;"
47+
d="M172.517,141.7c-0.288,2.039,3.733,7.48,8.976,8.207c5.234,0.73,9.714-3.522,9.998-5.559c0.284-2.039-3.732-4.285-8.977-5.015c-5.237-0.731-9.719,0.333-9.996,2.367z"
48+
/>
49+
<path
50+
style="fill:#FFFFFF;stroke-width:2.0775;stroke-linecap:butt;stroke-linejoin:miter;"
51+
d="M331.941,137.543c0.284,2.039-3.732,7.48-8.976,8.207c-5.238,0.73-9.718-3.522-10.005-5.559c-0.277-2.039,3.74-4.285,8.979-5.015c5.239-0.73,9.718,0.333,10.002,2.368z"
52+
/>
53+
<path
54+
d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"
55+
/>
56+
<path style="stroke-width:3;" d="M0,60.232" />
57+
</g>
58+
</svg>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<script lang="ts">
2+
import { ExternalLink } from "lucide-svelte";
3+
import { Card, CardContent, CardTitle } from "$lib/components/ui/card";
4+
import CardHeader from "$lib/components/ui/card/card-header.svelte";
5+
import type { PostgresSinkConsumer } from "../../consumers/types";
6+
7+
export let consumer: PostgresSinkConsumer;
8+
9+
function getRoutingCode(consumer: PostgresSinkConsumer): string | null {
10+
if (!consumer.routing || !consumer.routing.function) return null;
11+
const func = consumer.routing.function;
12+
if (func.type === "routing" && "code" in func) {
13+
return func.code;
14+
}
15+
return null;
16+
}
17+
</script>
18+
19+
<Card>
20+
<CardContent class="p-6">
21+
<div class="flex justify-between items-center mb-4">
22+
<h2 class="text-lg font-semibold">PostgreSQL Configuration</h2>
23+
</div>
24+
25+
<div class="grid gap-4">
26+
<div>
27+
<span class="text-sm text-muted-foreground">Host</span>
28+
<div class="mt-2">
29+
<div
30+
class="font-mono bg-slate-50 py-1 px-2 border border-slate-100 rounded-md break-all w-fit"
31+
>
32+
<span>{consumer.sink.host}</span>
33+
</div>
34+
</div>
35+
</div>
36+
37+
<div>
38+
<span class="text-sm text-muted-foreground">Port</span>
39+
<div class="mt-2">
40+
<span
41+
class="font-mono bg-slate-50 py-1 px-2 border border-slate-100 rounded-md whitespace-nowrap"
42+
>{consumer.sink.port}</span
43+
>
44+
</div>
45+
</div>
46+
47+
<div>
48+
<span class="text-sm text-muted-foreground">Database</span>
49+
<div class="mt-2">
50+
<span
51+
class="font-mono bg-slate-50 py-1 px-2 border border-slate-100 rounded-md whitespace-nowrap"
52+
>{consumer.sink.database}</span
53+
>
54+
</div>
55+
</div>
56+
57+
<div>
58+
<span class="text-sm text-muted-foreground">Table Name</span>
59+
<div class="mt-2">
60+
<span
61+
class="font-mono bg-slate-50 py-1 px-2 border border-slate-100 rounded-md whitespace-nowrap"
62+
>{consumer.sink.table_name}</span
63+
>
64+
</div>
65+
</div>
66+
67+
<div>
68+
<span class="text-sm text-muted-foreground">SSL Enabled</span>
69+
<div class="mt-2">
70+
<span
71+
class="font-mono bg-slate-50 py-1 px-2 border border-slate-100 rounded-md whitespace-nowrap"
72+
>{consumer.sink.ssl ? "Yes" : "No"}</span
73+
>
74+
</div>
75+
</div>
76+
</div>
77+
</CardContent>
78+
</Card>
79+
80+
<Card>
81+
<CardHeader>
82+
<CardTitle>Routing</CardTitle>
83+
</CardHeader>
84+
<CardContent>
85+
<div>
86+
<span class="text-sm text-gray-500">Table</span>
87+
<div class="mt-2">
88+
<span
89+
class="font-mono bg-slate-50 px-2 py-1 border border-slate-100 rounded-md whitespace-nowrap"
90+
>
91+
{#if consumer.routing_id}
92+
Determined by <a
93+
href={`/functions/${consumer.routing_id}`}
94+
data-phx-link="redirect"
95+
data-phx-link-state="push"
96+
class="underline">router</a
97+
>
98+
<ExternalLink class="h-4 w-4 inline" />
99+
{:else}
100+
{consumer.sink.table_name}
101+
{/if}
102+
</span>
103+
</div>
104+
</div>
105+
{#if consumer.routing}
106+
{#if getRoutingCode(consumer)}
107+
<div class="mt-2">
108+
<span class="text-sm text-gray-500">Router</span>
109+
<div class="mt-2">
110+
<pre
111+
class="font-mono bg-slate-50 p-2 border border-slate-100 rounded-md text-sm overflow-x-auto"><code
112+
>{getRoutingCode(consumer)}</code
113+
></pre>
114+
</div>
115+
</div>
116+
{/if}
117+
{/if}
118+
</CardContent>
119+
</Card>

0 commit comments

Comments
 (0)