realtimex-crm 0.8.1 → 0.9.1

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 (29) hide show
  1. package/dist/assets/{DealList-B3alafQv.js → DealList-DnGVfS15.js} +2 -2
  2. package/dist/assets/{DealList-B3alafQv.js.map → DealList-DnGVfS15.js.map} +1 -1
  3. package/dist/assets/index-DPrpo5Xq.js +159 -0
  4. package/dist/assets/index-DPrpo5Xq.js.map +1 -0
  5. package/dist/assets/index-kM1Og1AS.css +1 -0
  6. package/dist/index.html +1 -1
  7. package/dist/stats.html +1 -1
  8. package/package.json +2 -1
  9. package/src/components/atomic-crm/integrations/ApiKeysTab.tsx +184 -0
  10. package/src/components/atomic-crm/integrations/CreateApiKeyDialog.tsx +217 -0
  11. package/src/components/atomic-crm/integrations/IntegrationsPage.tsx +34 -0
  12. package/src/components/atomic-crm/integrations/WebhooksTab.tsx +402 -0
  13. package/src/components/atomic-crm/layout/Header.tsx +14 -1
  14. package/src/components/atomic-crm/root/CRM.tsx +2 -0
  15. package/src/components/ui/alert-dialog.tsx +155 -0
  16. package/src/lib/api-key-utils.ts +22 -0
  17. package/supabase/functions/_shared/apiKeyAuth.ts +171 -0
  18. package/supabase/functions/_shared/webhookSignature.ts +23 -0
  19. package/supabase/functions/api-v1-activities/index.ts +137 -0
  20. package/supabase/functions/api-v1-companies/index.ts +166 -0
  21. package/supabase/functions/api-v1-contacts/index.ts +171 -0
  22. package/supabase/functions/api-v1-deals/index.ts +166 -0
  23. package/supabase/functions/webhook-dispatcher/index.ts +133 -0
  24. package/supabase/migrations/20251219120000_api_integrations.sql +133 -0
  25. package/supabase/migrations/20251219120100_webhook_triggers.sql +171 -0
  26. package/supabase/migrations/20251219120200_webhook_cron.sql +26 -0
  27. package/dist/assets/index-CHk72bAf.css +0 -1
  28. package/dist/assets/index-mhAjQ1_w.js +0 -153
  29. package/dist/assets/index-mhAjQ1_w.js.map +0 -1
@@ -0,0 +1,166 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
3
+ import { corsHeaders, createErrorResponse } from "../_shared/utils.ts";
4
+ import {
5
+ validateApiKey,
6
+ checkRateLimit,
7
+ hasScope,
8
+ logApiRequest,
9
+ } from "../_shared/apiKeyAuth.ts";
10
+
11
+ Deno.serve(async (req: Request) => {
12
+ const startTime = Date.now();
13
+
14
+ if (req.method === "OPTIONS") {
15
+ return new Response(null, { status: 204, headers: corsHeaders });
16
+ }
17
+
18
+ const authResult = await validateApiKey(req);
19
+ if ("status" in authResult) {
20
+ return authResult;
21
+ }
22
+ const { apiKey } = authResult;
23
+
24
+ const rateLimitError = checkRateLimit(apiKey.id);
25
+ if (rateLimitError) {
26
+ await logApiRequest(
27
+ apiKey.id,
28
+ "/v1/deals",
29
+ req.method,
30
+ 429,
31
+ Date.now() - startTime,
32
+ req
33
+ );
34
+ return rateLimitError;
35
+ }
36
+
37
+ try {
38
+ const url = new URL(req.url);
39
+ const pathParts = url.pathname.split("/").filter(Boolean);
40
+ const dealId = pathParts[1];
41
+
42
+ let response: Response;
43
+
44
+ if (req.method === "GET" && dealId) {
45
+ response = await getDeal(apiKey, dealId);
46
+ } else if (req.method === "POST") {
47
+ response = await createDeal(apiKey, req);
48
+ } else if (req.method === "PATCH" && dealId) {
49
+ response = await updateDeal(apiKey, dealId, req);
50
+ } else if (req.method === "DELETE" && dealId) {
51
+ response = await deleteDeal(apiKey, dealId);
52
+ } else {
53
+ response = createErrorResponse(404, "Not found");
54
+ }
55
+
56
+ const responseTime = Date.now() - startTime;
57
+ await logApiRequest(
58
+ apiKey.id,
59
+ url.pathname,
60
+ req.method,
61
+ response.status,
62
+ responseTime,
63
+ req
64
+ );
65
+
66
+ return response;
67
+ } catch (error) {
68
+ const responseTime = Date.now() - startTime;
69
+ await logApiRequest(
70
+ apiKey.id,
71
+ new URL(req.url).pathname,
72
+ req.method,
73
+ 500,
74
+ responseTime,
75
+ req,
76
+ error.message
77
+ );
78
+ return createErrorResponse(500, "Internal server error");
79
+ }
80
+ });
81
+
82
+ async function getDeal(apiKey: any, dealId: string) {
83
+ if (!hasScope(apiKey, "deals:read")) {
84
+ return createErrorResponse(403, "Insufficient permissions");
85
+ }
86
+
87
+ const { data, error } = await supabaseAdmin
88
+ .from("deals")
89
+ .select("*")
90
+ .eq("id", dealId)
91
+ .single();
92
+
93
+ if (error || !data) {
94
+ return createErrorResponse(404, "Deal not found");
95
+ }
96
+
97
+ return new Response(JSON.stringify({ data }), {
98
+ headers: { "Content-Type": "application/json", ...corsHeaders },
99
+ });
100
+ }
101
+
102
+ async function createDeal(apiKey: any, req: Request) {
103
+ if (!hasScope(apiKey, "deals:write")) {
104
+ return createErrorResponse(403, "Insufficient permissions");
105
+ }
106
+
107
+ const body = await req.json();
108
+
109
+ const { data, error } = await supabaseAdmin
110
+ .from("deals")
111
+ .insert({
112
+ ...body,
113
+ sales_id: apiKey.sales_id,
114
+ })
115
+ .select()
116
+ .single();
117
+
118
+ if (error) {
119
+ return createErrorResponse(400, error.message);
120
+ }
121
+
122
+ return new Response(JSON.stringify({ data }), {
123
+ status: 201,
124
+ headers: { "Content-Type": "application/json", ...corsHeaders },
125
+ });
126
+ }
127
+
128
+ async function updateDeal(apiKey: any, dealId: string, req: Request) {
129
+ if (!hasScope(apiKey, "deals:write")) {
130
+ return createErrorResponse(403, "Insufficient permissions");
131
+ }
132
+
133
+ const body = await req.json();
134
+
135
+ const { data, error } = await supabaseAdmin
136
+ .from("deals")
137
+ .update(body)
138
+ .eq("id", dealId)
139
+ .select()
140
+ .single();
141
+
142
+ if (error || !data) {
143
+ return createErrorResponse(404, "Deal not found");
144
+ }
145
+
146
+ return new Response(JSON.stringify({ data }), {
147
+ headers: { "Content-Type": "application/json", ...corsHeaders },
148
+ });
149
+ }
150
+
151
+ async function deleteDeal(apiKey: any, dealId: string) {
152
+ if (!hasScope(apiKey, "deals:write")) {
153
+ return createErrorResponse(403, "Insufficient permissions");
154
+ }
155
+
156
+ const { error } = await supabaseAdmin
157
+ .from("deals")
158
+ .delete()
159
+ .eq("id", dealId);
160
+
161
+ if (error) {
162
+ return createErrorResponse(404, "Deal not found");
163
+ }
164
+
165
+ return new Response(null, { status: 204, headers: corsHeaders });
166
+ }
@@ -0,0 +1,133 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
3
+ import { generateWebhookSignature } from "../_shared/webhookSignature.ts";
4
+
5
+ Deno.serve(async () => {
6
+ console.log("Webhook dispatcher running...");
7
+
8
+ // Get pending webhook deliveries
9
+ const { data: queueItems } = await supabaseAdmin
10
+ .from("webhook_queue")
11
+ .select("*, webhooks(*)")
12
+ .eq("status", "pending")
13
+ .lte("next_retry_at", new Date().toISOString())
14
+ .limit(50);
15
+
16
+ if (!queueItems || queueItems.length === 0) {
17
+ return new Response(JSON.stringify({ processed: 0 }), {
18
+ headers: { "Content-Type": "application/json" },
19
+ });
20
+ }
21
+
22
+ console.log(`Processing ${queueItems.length} webhook deliveries`);
23
+
24
+ const results = await Promise.allSettled(
25
+ queueItems.map((item) => deliverWebhook(item))
26
+ );
27
+
28
+ const successful = results.filter((r) => r.status === "fulfilled").length;
29
+ const failed = results.filter((r) => r.status === "rejected").length;
30
+
31
+ return new Response(
32
+ JSON.stringify({ processed: queueItems.length, successful, failed }),
33
+ { headers: { "Content-Type": "application/json" } }
34
+ );
35
+ });
36
+
37
+ async function deliverWebhook(queueItem: any) {
38
+ const webhook = queueItem.webhooks;
39
+
40
+ // Mark as processing
41
+ await supabaseAdmin
42
+ .from("webhook_queue")
43
+ .update({ status: "processing" })
44
+ .eq("id", queueItem.id);
45
+
46
+ try {
47
+ const payloadString = JSON.stringify(queueItem.payload);
48
+ const signature = await generateWebhookSignature(
49
+ webhook.secret,
50
+ payloadString
51
+ );
52
+
53
+ const headers: Record<string, string> = {
54
+ "Content-Type": "application/json",
55
+ "X-Webhook-Signature": signature,
56
+ "X-Webhook-Event": queueItem.event_type,
57
+ "User-Agent": "AtomicCRM-Webhooks/1.0",
58
+ };
59
+
60
+ // Add custom headers from webhook config
61
+ if (webhook.headers) {
62
+ Object.assign(headers, webhook.headers);
63
+ }
64
+
65
+ const response = await fetch(webhook.url, {
66
+ method: "POST",
67
+ headers,
68
+ body: payloadString,
69
+ signal: AbortSignal.timeout(30000), // 30 second timeout
70
+ });
71
+
72
+ if (response.ok) {
73
+ // Success
74
+ await supabaseAdmin
75
+ .from("webhook_queue")
76
+ .update({
77
+ status: "delivered",
78
+ delivered_at: new Date().toISOString(),
79
+ })
80
+ .eq("id", queueItem.id);
81
+
82
+ await supabaseAdmin
83
+ .from("webhooks")
84
+ .update({
85
+ last_triggered_at: new Date().toISOString(),
86
+ failure_count: 0,
87
+ })
88
+ .eq("id", webhook.id);
89
+
90
+ console.log(`Webhook ${queueItem.id} delivered successfully`);
91
+ } else {
92
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
93
+ }
94
+ } catch (error) {
95
+ console.error(`Webhook ${queueItem.id} delivery failed:`, error);
96
+
97
+ const newAttempts = queueItem.attempts + 1;
98
+ const shouldRetry = newAttempts < queueItem.max_attempts;
99
+
100
+ if (shouldRetry) {
101
+ // Exponential backoff: 1min, 5min, 15min
102
+ const retryDelays = [60, 300, 900];
103
+ const delaySeconds = retryDelays[newAttempts - 1] || 900;
104
+ const nextRetry = new Date(Date.now() + delaySeconds * 1000)
105
+ .toISOString();
106
+
107
+ await supabaseAdmin
108
+ .from("webhook_queue")
109
+ .update({
110
+ status: "pending",
111
+ attempts: newAttempts,
112
+ next_retry_at: nextRetry,
113
+ error_message: error.message,
114
+ })
115
+ .eq("id", queueItem.id);
116
+ } else {
117
+ // Max attempts reached
118
+ await supabaseAdmin
119
+ .from("webhook_queue")
120
+ .update({
121
+ status: "failed",
122
+ attempts: newAttempts,
123
+ error_message: error.message,
124
+ })
125
+ .eq("id", queueItem.id);
126
+
127
+ await supabaseAdmin
128
+ .from("webhooks")
129
+ .update({ failure_count: webhook.failure_count + 1 })
130
+ .eq("id", webhook.id);
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,133 @@
1
+ -- API Keys table
2
+ create table "public"."api_keys" (
3
+ "id" bigint generated by default as identity not null,
4
+ "created_at" timestamp with time zone not null default now(),
5
+ "sales_id" bigint not null,
6
+ "name" text not null,
7
+ "key_hash" text not null,
8
+ "key_prefix" text not null,
9
+ "scopes" text[] not null default '{}',
10
+ "is_active" boolean not null default true,
11
+ "expires_at" timestamp with time zone,
12
+ "last_used_at" timestamp with time zone,
13
+ "created_by_sales_id" bigint not null
14
+ );
15
+
16
+ alter table "public"."api_keys" enable row level security;
17
+
18
+ CREATE UNIQUE INDEX api_keys_pkey ON public.api_keys USING btree (id);
19
+ CREATE UNIQUE INDEX api_keys_key_hash_unique ON public.api_keys USING btree (key_hash);
20
+ CREATE INDEX api_keys_sales_id_idx ON public.api_keys USING btree (sales_id);
21
+
22
+ alter table "public"."api_keys" add constraint "api_keys_pkey" PRIMARY KEY using index "api_keys_pkey";
23
+ alter table "public"."api_keys" add constraint "api_keys_sales_id_fkey"
24
+ FOREIGN KEY (sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
25
+ alter table "public"."api_keys" validate constraint "api_keys_sales_id_fkey";
26
+ alter table "public"."api_keys" add constraint "api_keys_created_by_sales_id_fkey"
27
+ FOREIGN KEY (created_by_sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
28
+ alter table "public"."api_keys" validate constraint "api_keys_created_by_sales_id_fkey";
29
+
30
+ -- Webhooks table
31
+ create table "public"."webhooks" (
32
+ "id" bigint generated by default as identity not null,
33
+ "created_at" timestamp with time zone not null default now(),
34
+ "sales_id" bigint not null,
35
+ "name" text not null,
36
+ "url" text not null,
37
+ "events" text[] not null default '{}',
38
+ "headers" jsonb,
39
+ "is_active" boolean not null default true,
40
+ "secret" text not null,
41
+ "last_triggered_at" timestamp with time zone,
42
+ "failure_count" int not null default 0,
43
+ "created_by_sales_id" bigint not null
44
+ );
45
+
46
+ alter table "public"."webhooks" enable row level security;
47
+
48
+ CREATE UNIQUE INDEX webhooks_pkey ON public.webhooks USING btree (id);
49
+ CREATE INDEX webhooks_sales_id_idx ON public.webhooks USING btree (sales_id);
50
+ CREATE INDEX webhooks_events_idx ON public.webhooks USING gin (events);
51
+
52
+ alter table "public"."webhooks" add constraint "webhooks_pkey" PRIMARY KEY using index "webhooks_pkey";
53
+ alter table "public"."webhooks" add constraint "webhooks_sales_id_fkey"
54
+ FOREIGN KEY (sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
55
+ alter table "public"."webhooks" validate constraint "webhooks_sales_id_fkey";
56
+ alter table "public"."webhooks" add constraint "webhooks_created_by_sales_id_fkey"
57
+ FOREIGN KEY (created_by_sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
58
+ alter table "public"."webhooks" validate constraint "webhooks_created_by_sales_id_fkey";
59
+
60
+ -- API Request Logs table
61
+ create table "public"."api_logs" (
62
+ "id" bigint generated by default as identity not null,
63
+ "created_at" timestamp with time zone not null default now(),
64
+ "api_key_id" bigint not null,
65
+ "endpoint" text not null,
66
+ "method" text not null,
67
+ "status_code" int not null,
68
+ "response_time_ms" int,
69
+ "ip_address" text,
70
+ "user_agent" text,
71
+ "error_message" text
72
+ );
73
+
74
+ alter table "public"."api_logs" enable row level security;
75
+
76
+ CREATE UNIQUE INDEX api_logs_pkey ON public.api_logs USING btree (id);
77
+ CREATE INDEX api_logs_api_key_id_idx ON public.api_logs USING btree (api_key_id);
78
+ CREATE INDEX api_logs_created_at_idx ON public.api_logs USING btree (created_at DESC);
79
+
80
+ alter table "public"."api_logs" add constraint "api_logs_pkey" PRIMARY KEY using index "api_logs_pkey";
81
+ alter table "public"."api_logs" add constraint "api_logs_api_key_id_fkey"
82
+ FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
83
+ alter table "public"."api_logs" validate constraint "api_logs_api_key_id_fkey";
84
+
85
+ -- Webhook Queue table (for async delivery)
86
+ create table "public"."webhook_queue" (
87
+ "id" bigint generated by default as identity not null,
88
+ "created_at" timestamp with time zone not null default now(),
89
+ "webhook_id" bigint not null,
90
+ "event_type" text not null,
91
+ "payload" jsonb not null,
92
+ "status" text not null default 'pending',
93
+ "attempts" int not null default 0,
94
+ "max_attempts" int not null default 3,
95
+ "next_retry_at" timestamp with time zone,
96
+ "delivered_at" timestamp with time zone,
97
+ "error_message" text
98
+ );
99
+
100
+ alter table "public"."webhook_queue" enable row level security;
101
+
102
+ CREATE UNIQUE INDEX webhook_queue_pkey ON public.webhook_queue USING btree (id);
103
+ CREATE INDEX webhook_queue_status_idx ON public.webhook_queue USING btree (status, next_retry_at);
104
+ CREATE INDEX webhook_queue_webhook_id_idx ON public.webhook_queue USING btree (webhook_id);
105
+
106
+ alter table "public"."webhook_queue" add constraint "webhook_queue_pkey" PRIMARY KEY using index "webhook_queue_pkey";
107
+ alter table "public"."webhook_queue" add constraint "webhook_queue_webhook_id_fkey"
108
+ FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
109
+ alter table "public"."webhook_queue" validate constraint "webhook_queue_webhook_id_fkey";
110
+
111
+ -- Grant permissions
112
+ grant select, insert, update, delete on table "public"."api_keys" to "authenticated";
113
+ grant select, insert, update, delete on table "public"."webhooks" to "authenticated";
114
+ grant select on table "public"."api_logs" to "authenticated";
115
+ grant select on table "public"."webhook_queue" to "authenticated";
116
+
117
+ grant all on table "public"."api_keys" to "service_role";
118
+ grant all on table "public"."webhooks" to "service_role";
119
+ grant all on table "public"."api_logs" to "service_role";
120
+ grant all on table "public"."webhook_queue" to "service_role";
121
+
122
+ -- RLS Policies (permissive for authenticated users)
123
+ create policy "Enable all for authenticated users" on "public"."api_keys"
124
+ for all to authenticated using (true) with check (true);
125
+
126
+ create policy "Enable all for authenticated users" on "public"."webhooks"
127
+ for all to authenticated using (true) with check (true);
128
+
129
+ create policy "Enable read for authenticated users" on "public"."api_logs"
130
+ for select to authenticated using (true);
131
+
132
+ create policy "Enable read for authenticated users" on "public"."webhook_queue"
133
+ for select to authenticated using (true);
@@ -0,0 +1,171 @@
1
+ -- Function to enqueue webhook events
2
+ create or replace function enqueue_webhook_event(
3
+ p_event_type text,
4
+ p_payload jsonb
5
+ ) returns void
6
+ language plpgsql
7
+ security definer
8
+ set search_path = ''
9
+ as $$
10
+ begin
11
+ -- Find all active webhooks that listen to this event
12
+ insert into public.webhook_queue (webhook_id, event_type, payload, next_retry_at)
13
+ select
14
+ id,
15
+ p_event_type,
16
+ p_payload,
17
+ now()
18
+ from public.webhooks
19
+ where is_active = true
20
+ and p_event_type = any(events);
21
+ end;
22
+ $$;
23
+
24
+ -- Trigger function for contact CRUD events
25
+ create or replace function trigger_contact_webhooks()
26
+ returns trigger
27
+ language plpgsql
28
+ security definer
29
+ set search_path = ''
30
+ as $$
31
+ declare
32
+ event_type text;
33
+ payload jsonb;
34
+ begin
35
+ if (TG_OP = 'INSERT') then
36
+ event_type := 'contact.created';
37
+ payload := to_jsonb(NEW);
38
+ elsif (TG_OP = 'UPDATE') then
39
+ event_type := 'contact.updated';
40
+ payload := jsonb_build_object('old', to_jsonb(OLD), 'new', to_jsonb(NEW));
41
+ elsif (TG_OP = 'DELETE') then
42
+ event_type := 'contact.deleted';
43
+ payload := to_jsonb(OLD);
44
+ end if;
45
+
46
+ perform enqueue_webhook_event(event_type, payload);
47
+
48
+ if (TG_OP = 'DELETE') then
49
+ return OLD;
50
+ else
51
+ return NEW;
52
+ end if;
53
+ end;
54
+ $$;
55
+
56
+ -- Trigger function for company CRUD events
57
+ create or replace function trigger_company_webhooks()
58
+ returns trigger
59
+ language plpgsql
60
+ security definer
61
+ set search_path = ''
62
+ as $$
63
+ declare
64
+ event_type text;
65
+ payload jsonb;
66
+ begin
67
+ if (TG_OP = 'INSERT') then
68
+ event_type := 'company.created';
69
+ payload := to_jsonb(NEW);
70
+ elsif (TG_OP = 'UPDATE') then
71
+ event_type := 'company.updated';
72
+ payload := jsonb_build_object('old', to_jsonb(OLD), 'new', to_jsonb(NEW));
73
+ elsif (TG_OP = 'DELETE') then
74
+ event_type := 'company.deleted';
75
+ payload := to_jsonb(OLD);
76
+ end if;
77
+
78
+ perform enqueue_webhook_event(event_type, payload);
79
+
80
+ if (TG_OP = 'DELETE') then
81
+ return OLD;
82
+ else
83
+ return NEW;
84
+ end if;
85
+ end;
86
+ $$;
87
+
88
+ -- Trigger function for deal CRUD and stage change events
89
+ create or replace function trigger_deal_webhooks()
90
+ returns trigger
91
+ language plpgsql
92
+ security definer
93
+ set search_path = ''
94
+ as $$
95
+ declare
96
+ event_type text;
97
+ payload jsonb;
98
+ begin
99
+ if (TG_OP = 'INSERT') then
100
+ event_type := 'deal.created';
101
+ payload := to_jsonb(NEW);
102
+ perform enqueue_webhook_event(event_type, payload);
103
+ elsif (TG_OP = 'UPDATE') then
104
+ -- Check for stage changes
105
+ if (OLD.stage <> NEW.stage) then
106
+ event_type := 'deal.stage_changed';
107
+ payload := jsonb_build_object(
108
+ 'deal_id', NEW.id,
109
+ 'old_stage', OLD.stage,
110
+ 'new_stage', NEW.stage,
111
+ 'deal', to_jsonb(NEW)
112
+ );
113
+ perform enqueue_webhook_event(event_type, payload);
114
+
115
+ -- Check for won/lost
116
+ if (NEW.stage = 'won') then
117
+ perform enqueue_webhook_event('deal.won', to_jsonb(NEW));
118
+ elsif (NEW.stage = 'lost') then
119
+ perform enqueue_webhook_event('deal.lost', to_jsonb(NEW));
120
+ end if;
121
+ end if;
122
+
123
+ event_type := 'deal.updated';
124
+ payload := jsonb_build_object('old', to_jsonb(OLD), 'new', to_jsonb(NEW));
125
+ perform enqueue_webhook_event(event_type, payload);
126
+ elsif (TG_OP = 'DELETE') then
127
+ event_type := 'deal.deleted';
128
+ payload := to_jsonb(OLD);
129
+ perform enqueue_webhook_event(event_type, payload);
130
+ end if;
131
+
132
+ if (TG_OP = 'DELETE') then
133
+ return OLD;
134
+ else
135
+ return NEW;
136
+ end if;
137
+ end;
138
+ $$;
139
+
140
+ -- Trigger function for task completion
141
+ create or replace function trigger_task_webhooks()
142
+ returns trigger
143
+ language plpgsql
144
+ security definer
145
+ set search_path = ''
146
+ as $$
147
+ begin
148
+ if (TG_OP = 'UPDATE' and OLD.done_date is null and NEW.done_date is not null) then
149
+ perform enqueue_webhook_event('task.completed', to_jsonb(NEW));
150
+ end if;
151
+
152
+ return NEW;
153
+ end;
154
+ $$;
155
+
156
+ -- Create triggers
157
+ create trigger contact_webhook_trigger
158
+ after insert or update or delete on public.contacts
159
+ for each row execute function trigger_contact_webhooks();
160
+
161
+ create trigger company_webhook_trigger
162
+ after insert or update or delete on public.companies
163
+ for each row execute function trigger_company_webhooks();
164
+
165
+ create trigger deal_webhook_trigger
166
+ after insert or update or delete on public.deals
167
+ for each row execute function trigger_deal_webhooks();
168
+
169
+ create trigger task_webhook_trigger
170
+ after update on public.tasks
171
+ for each row execute function trigger_task_webhooks();
@@ -0,0 +1,26 @@
1
+ -- Enable pg_cron extension if not already enabled
2
+ create extension if not exists pg_cron with schema extensions;
3
+
4
+ -- Schedule webhook dispatcher to run every minute
5
+ -- Note: This uses Supabase's http extension to call the Edge Function
6
+ select cron.schedule(
7
+ 'webhook-dispatcher',
8
+ '* * * * *', -- Every minute
9
+ $$
10
+ select
11
+ net.http_post(
12
+ url:='https://' || current_setting('app.settings.supabase_url', true) || '/functions/v1/webhook-dispatcher',
13
+ headers:=jsonb_build_object(
14
+ 'Content-Type','application/json',
15
+ 'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key', true)
16
+ ),
17
+ body:='{}'::jsonb
18
+ ) as request_id;
19
+ $$
20
+ );
21
+
22
+ -- Note: To configure this cron job properly, you need to set the following settings in your Supabase project:
23
+ -- ALTER DATABASE postgres SET app.settings.supabase_url = 'your-project-ref.supabase.co';
24
+ -- ALTER DATABASE postgres SET app.settings.service_role_key = 'your-service-role-key';
25
+ --
26
+ -- Alternatively, the webhook dispatcher can be called manually or via a separate cron service like GitHub Actions or a cloud scheduler.