writbase 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.
- package/README.md +121 -0
- package/dist/writbase.js +707 -0
- package/migrations/00001_enums.sql +7 -0
- package/migrations/00002_core_tables.sql +105 -0
- package/migrations/00003_constraints_indexes.sql +69 -0
- package/migrations/00004_rls_policies.sql +169 -0
- package/migrations/00005_seed_app_settings.sql +2 -0
- package/migrations/00006_rate_limit_rpc.sql +14 -0
- package/migrations/00007_rate_limit_rpc_search_path.sql +14 -0
- package/migrations/00008_indexes_and_cleanup.sql +22 -0
- package/migrations/00009_atomic_mutations.sql +175 -0
- package/migrations/00010_user_rate_limits.sql +52 -0
- package/migrations/00011_atomic_permission_update.sql +31 -0
- package/migrations/00012_pg_cron_cleanup.sql +27 -0
- package/migrations/00013_max_agent_keys.sql +4 -0
- package/migrations/00014_event_log_indexes.sql +2 -0
- package/migrations/00015_auth_rate_limits.sql +69 -0
- package/migrations/00016_request_log.sql +42 -0
- package/migrations/00017_task_search.sql +58 -0
- package/migrations/00018_inter_agent_exchange.sql +423 -0
- package/migrations/00019_workspaces.sql +782 -0
- package/migrations/00020_can_comment.sql +46 -0
- package/migrations/00021_webhook_delivery.sql +125 -0
- package/migrations/00022_task_archive.sql +380 -0
- package/migrations/00023_get_top_tasks.sql +21 -0
- package/migrations/00024_agent_key_defaults.sql +55 -0
- package/migrations/00025_production_automation.sql +353 -0
- package/migrations/00026_top_tasks_exclude_terminal.sql +19 -0
- package/migrations/00027_rename_agent_key_defaults.sql +51 -0
- package/package.json +34 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
-- Production automation: 6 pg_cron jobs for operational hygiene
|
|
2
|
+
--
|
|
3
|
+
-- 1. Auto-archive stale completed tasks (daily)
|
|
4
|
+
-- 2. Deactivate unused agent keys (weekly)
|
|
5
|
+
-- 3. Event log rotation (daily)
|
|
6
|
+
-- 4. Webhook delivery retries + dead-letter cleanup (every 2 min + daily)
|
|
7
|
+
-- 5. Usage metrics aggregation (daily)
|
|
8
|
+
-- 6. ANALYZE hot tables (daily)
|
|
9
|
+
--
|
|
10
|
+
-- All jobs are gated on pg_cron availability (no-op on local dev).
|
|
11
|
+
|
|
12
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
13
|
+
-- 1. Auto-archive: mark completed/cancelled/failed tasks as archived
|
|
14
|
+
-- after 30 days of inactivity
|
|
15
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
CREATE OR REPLACE FUNCTION auto_archive_stale_tasks()
|
|
18
|
+
RETURNS integer
|
|
19
|
+
LANGUAGE plpgsql
|
|
20
|
+
SET search_path = public
|
|
21
|
+
AS $$
|
|
22
|
+
DECLARE
|
|
23
|
+
v_count integer;
|
|
24
|
+
BEGIN
|
|
25
|
+
UPDATE tasks
|
|
26
|
+
SET is_archived = true, updated_at = now()
|
|
27
|
+
WHERE status IN ('done', 'cancelled', 'failed')
|
|
28
|
+
AND is_archived = false
|
|
29
|
+
AND updated_at < now() - interval '30 days';
|
|
30
|
+
|
|
31
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
32
|
+
RETURN v_count;
|
|
33
|
+
END;
|
|
34
|
+
$$;
|
|
35
|
+
|
|
36
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
37
|
+
-- 2. Inactive key detection: deactivate keys unused for 90+ days
|
|
38
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
CREATE OR REPLACE FUNCTION deactivate_stale_agent_keys()
|
|
41
|
+
RETURNS integer
|
|
42
|
+
LANGUAGE plpgsql
|
|
43
|
+
SET search_path = public
|
|
44
|
+
AS $$
|
|
45
|
+
DECLARE
|
|
46
|
+
v_count integer;
|
|
47
|
+
BEGIN
|
|
48
|
+
UPDATE agent_keys
|
|
49
|
+
SET is_active = false
|
|
50
|
+
WHERE is_active = true
|
|
51
|
+
AND last_used_at IS NOT NULL
|
|
52
|
+
AND last_used_at < now() - interval '90 days';
|
|
53
|
+
|
|
54
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
55
|
+
RETURN v_count;
|
|
56
|
+
END;
|
|
57
|
+
$$;
|
|
58
|
+
|
|
59
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
60
|
+
-- 3. Event log rotation: delete entries older than 90 days
|
|
61
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
62
|
+
|
|
63
|
+
-- The event_log trigger (prevent_event_log_mutation) fires unconditionally
|
|
64
|
+
-- on UPDATE/DELETE — SECURITY DEFINER does NOT bypass triggers. We add a
|
|
65
|
+
-- session variable check so the cleanup function can bypass it safely.
|
|
66
|
+
|
|
67
|
+
CREATE OR REPLACE FUNCTION prevent_event_log_mutation() RETURNS TRIGGER AS $$
|
|
68
|
+
BEGIN
|
|
69
|
+
-- Allow cleanup jobs to bypass via session variable
|
|
70
|
+
IF current_setting('app.allow_event_log_cleanup', true) = 'true' THEN
|
|
71
|
+
RETURN OLD;
|
|
72
|
+
END IF;
|
|
73
|
+
RAISE EXCEPTION 'event_log is append-only: % not permitted', TG_OP;
|
|
74
|
+
END;
|
|
75
|
+
$$ LANGUAGE plpgsql;
|
|
76
|
+
|
|
77
|
+
CREATE OR REPLACE FUNCTION cleanup_event_log()
|
|
78
|
+
RETURNS integer
|
|
79
|
+
LANGUAGE plpgsql
|
|
80
|
+
SECURITY DEFINER
|
|
81
|
+
SET search_path = public
|
|
82
|
+
AS $$
|
|
83
|
+
DECLARE
|
|
84
|
+
v_count integer;
|
|
85
|
+
BEGIN
|
|
86
|
+
-- Set bypass flag for the append-only trigger
|
|
87
|
+
PERFORM set_config('app.allow_event_log_cleanup', 'true', true);
|
|
88
|
+
|
|
89
|
+
DELETE FROM event_log
|
|
90
|
+
WHERE created_at < now() - interval '90 days';
|
|
91
|
+
|
|
92
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
93
|
+
|
|
94
|
+
-- Reset bypass flag
|
|
95
|
+
PERFORM set_config('app.allow_event_log_cleanup', 'false', true);
|
|
96
|
+
RETURN v_count;
|
|
97
|
+
END;
|
|
98
|
+
$$;
|
|
99
|
+
|
|
100
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
101
|
+
-- 4. Webhook delivery log + retry infrastructure
|
|
102
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
103
|
+
|
|
104
|
+
CREATE TABLE IF NOT EXISTS webhook_delivery_log (
|
|
105
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
106
|
+
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
107
|
+
subscription_id uuid NOT NULL REFERENCES webhook_subscriptions(id) ON DELETE CASCADE,
|
|
108
|
+
task_id uuid NOT NULL,
|
|
109
|
+
event_type text NOT NULL,
|
|
110
|
+
payload jsonb NOT NULL,
|
|
111
|
+
status text NOT NULL DEFAULT 'pending'
|
|
112
|
+
CHECK (status IN ('pending', 'delivered', 'failed', 'dead')),
|
|
113
|
+
attempts integer NOT NULL DEFAULT 0,
|
|
114
|
+
last_attempt_at timestamptz,
|
|
115
|
+
next_retry_at timestamptz DEFAULT now(),
|
|
116
|
+
last_error text,
|
|
117
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
CREATE INDEX idx_wdl_pending_retry
|
|
121
|
+
ON webhook_delivery_log (next_retry_at)
|
|
122
|
+
WHERE status = 'pending' OR status = 'failed';
|
|
123
|
+
|
|
124
|
+
CREATE INDEX idx_wdl_subscription
|
|
125
|
+
ON webhook_delivery_log (subscription_id, created_at DESC);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX idx_wdl_task
|
|
128
|
+
ON webhook_delivery_log (task_id);
|
|
129
|
+
|
|
130
|
+
-- Retry function: exponential backoff (2^attempts minutes, max 6 attempts)
|
|
131
|
+
-- After 6 failed attempts → mark as 'dead'
|
|
132
|
+
CREATE OR REPLACE FUNCTION process_webhook_retries()
|
|
133
|
+
RETURNS integer
|
|
134
|
+
LANGUAGE plpgsql
|
|
135
|
+
SECURITY DEFINER
|
|
136
|
+
SET search_path = public
|
|
137
|
+
AS $$
|
|
138
|
+
DECLARE
|
|
139
|
+
v_count integer := 0;
|
|
140
|
+
v_dead integer := 0;
|
|
141
|
+
v_stuck integer := 0;
|
|
142
|
+
BEGIN
|
|
143
|
+
-- Promote stuck pending rows (attempted but no update in 10 min) to failed
|
|
144
|
+
UPDATE webhook_delivery_log
|
|
145
|
+
SET status = 'failed'
|
|
146
|
+
WHERE status = 'pending'
|
|
147
|
+
AND attempts > 0
|
|
148
|
+
AND last_attempt_at < now() - interval '10 minutes';
|
|
149
|
+
GET DIAGNOSTICS v_stuck = ROW_COUNT;
|
|
150
|
+
|
|
151
|
+
-- Mark deliveries that exceeded max attempts as dead
|
|
152
|
+
UPDATE webhook_delivery_log
|
|
153
|
+
SET status = 'dead'
|
|
154
|
+
WHERE status = 'failed'
|
|
155
|
+
AND attempts >= 6;
|
|
156
|
+
GET DIAGNOSTICS v_dead = ROW_COUNT;
|
|
157
|
+
|
|
158
|
+
-- Reset eligible failed deliveries back to pending for retry
|
|
159
|
+
UPDATE webhook_delivery_log
|
|
160
|
+
SET status = 'pending',
|
|
161
|
+
next_retry_at = now() + (power(2, attempts) || ' minutes')::interval
|
|
162
|
+
WHERE status = 'failed'
|
|
163
|
+
AND attempts < 6
|
|
164
|
+
AND next_retry_at <= now();
|
|
165
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
166
|
+
|
|
167
|
+
RETURN v_stuck + v_dead + v_count;
|
|
168
|
+
END;
|
|
169
|
+
$$;
|
|
170
|
+
|
|
171
|
+
-- Dead-letter cleanup: purge dead entries older than 30 days
|
|
172
|
+
CREATE OR REPLACE FUNCTION cleanup_dead_webhooks()
|
|
173
|
+
RETURNS integer
|
|
174
|
+
LANGUAGE plpgsql
|
|
175
|
+
SECURITY DEFINER
|
|
176
|
+
SET search_path = public
|
|
177
|
+
AS $$
|
|
178
|
+
DECLARE
|
|
179
|
+
v_dead integer;
|
|
180
|
+
v_delivered integer;
|
|
181
|
+
BEGIN
|
|
182
|
+
DELETE FROM webhook_delivery_log
|
|
183
|
+
WHERE status = 'dead'
|
|
184
|
+
AND created_at < now() - interval '30 days';
|
|
185
|
+
GET DIAGNOSTICS v_dead = ROW_COUNT;
|
|
186
|
+
|
|
187
|
+
-- Also purge delivered entries older than 7 days (no longer needed)
|
|
188
|
+
DELETE FROM webhook_delivery_log
|
|
189
|
+
WHERE status = 'delivered'
|
|
190
|
+
AND created_at < now() - interval '7 days';
|
|
191
|
+
GET DIAGNOSTICS v_delivered = ROW_COUNT;
|
|
192
|
+
|
|
193
|
+
RETURN v_dead + v_delivered;
|
|
194
|
+
END;
|
|
195
|
+
$$;
|
|
196
|
+
|
|
197
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
198
|
+
-- 5. Usage metrics aggregation (for billing/quota enforcement)
|
|
199
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
200
|
+
|
|
201
|
+
CREATE TABLE IF NOT EXISTS workspace_usage (
|
|
202
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
203
|
+
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
|
204
|
+
period_start date NOT NULL,
|
|
205
|
+
period_end date NOT NULL,
|
|
206
|
+
tasks_created integer NOT NULL DEFAULT 0,
|
|
207
|
+
tasks_active integer NOT NULL DEFAULT 0,
|
|
208
|
+
api_requests integer NOT NULL DEFAULT 0,
|
|
209
|
+
agent_keys_active integer NOT NULL DEFAULT 0,
|
|
210
|
+
computed_at timestamptz NOT NULL DEFAULT now(),
|
|
211
|
+
UNIQUE (workspace_id, period_start)
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
CREATE INDEX idx_workspace_usage_lookup
|
|
215
|
+
ON workspace_usage (workspace_id, period_start DESC);
|
|
216
|
+
|
|
217
|
+
CREATE OR REPLACE FUNCTION aggregate_workspace_usage()
|
|
218
|
+
RETURNS integer
|
|
219
|
+
LANGUAGE plpgsql
|
|
220
|
+
SECURITY DEFINER
|
|
221
|
+
SET search_path = public
|
|
222
|
+
AS $$
|
|
223
|
+
DECLARE
|
|
224
|
+
v_period_start date;
|
|
225
|
+
v_period_end date;
|
|
226
|
+
v_count integer;
|
|
227
|
+
BEGIN
|
|
228
|
+
-- Current calendar month
|
|
229
|
+
v_period_start := date_trunc('month', now())::date;
|
|
230
|
+
v_period_end := (date_trunc('month', now()) + interval '1 month' - interval '1 day')::date;
|
|
231
|
+
|
|
232
|
+
INSERT INTO workspace_usage (workspace_id, period_start, period_end,
|
|
233
|
+
tasks_created, tasks_active, api_requests, agent_keys_active, computed_at)
|
|
234
|
+
SELECT
|
|
235
|
+
w.id,
|
|
236
|
+
v_period_start,
|
|
237
|
+
v_period_end,
|
|
238
|
+
COALESCE(tc.cnt, 0),
|
|
239
|
+
COALESCE(ta.cnt, 0),
|
|
240
|
+
COALESCE(rl.cnt, 0),
|
|
241
|
+
COALESCE(ak.cnt, 0),
|
|
242
|
+
now()
|
|
243
|
+
FROM workspaces w
|
|
244
|
+
LEFT JOIN LATERAL (
|
|
245
|
+
SELECT count(*)::int AS cnt FROM tasks
|
|
246
|
+
WHERE workspace_id = w.id
|
|
247
|
+
AND created_at >= v_period_start
|
|
248
|
+
AND created_at < v_period_end + interval '1 day'
|
|
249
|
+
) tc ON true
|
|
250
|
+
LEFT JOIN LATERAL (
|
|
251
|
+
SELECT count(*)::int AS cnt FROM tasks
|
|
252
|
+
WHERE workspace_id = w.id
|
|
253
|
+
AND is_archived = false
|
|
254
|
+
) ta ON true
|
|
255
|
+
LEFT JOIN LATERAL (
|
|
256
|
+
SELECT count(*)::int AS cnt FROM request_log
|
|
257
|
+
WHERE workspace_id = w.id
|
|
258
|
+
AND created_at >= v_period_start
|
|
259
|
+
AND created_at < v_period_end + interval '1 day'
|
|
260
|
+
) rl ON true
|
|
261
|
+
LEFT JOIN LATERAL (
|
|
262
|
+
SELECT count(*)::int AS cnt FROM agent_keys
|
|
263
|
+
WHERE workspace_id = w.id
|
|
264
|
+
AND is_active = true
|
|
265
|
+
) ak ON true
|
|
266
|
+
ON CONFLICT (workspace_id, period_start)
|
|
267
|
+
DO UPDATE SET
|
|
268
|
+
tasks_created = EXCLUDED.tasks_created,
|
|
269
|
+
tasks_active = EXCLUDED.tasks_active,
|
|
270
|
+
api_requests = EXCLUDED.api_requests,
|
|
271
|
+
agent_keys_active = EXCLUDED.agent_keys_active,
|
|
272
|
+
computed_at = now();
|
|
273
|
+
|
|
274
|
+
GET DIAGNOSTICS v_count = ROW_COUNT;
|
|
275
|
+
RETURN v_count;
|
|
276
|
+
END;
|
|
277
|
+
$$;
|
|
278
|
+
|
|
279
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
280
|
+
-- 6. Schedule all jobs via pg_cron
|
|
281
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
282
|
+
|
|
283
|
+
DO $outer$
|
|
284
|
+
BEGIN
|
|
285
|
+
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'cron') THEN
|
|
286
|
+
|
|
287
|
+
-- 1. Auto-archive stale tasks: daily at 02:00 UTC
|
|
288
|
+
PERFORM cron.schedule(
|
|
289
|
+
'auto-archive-stale-tasks',
|
|
290
|
+
'0 2 * * *',
|
|
291
|
+
'SELECT public.auto_archive_stale_tasks()'
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
-- 2. Deactivate stale agent keys: weekly on Sunday at 03:00 UTC
|
|
295
|
+
PERFORM cron.schedule(
|
|
296
|
+
'deactivate-stale-keys',
|
|
297
|
+
'0 3 * * 0',
|
|
298
|
+
'SELECT public.deactivate_stale_agent_keys()'
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
-- 3. Event log rotation: daily at 04:30 UTC
|
|
302
|
+
PERFORM cron.schedule(
|
|
303
|
+
'cleanup-event-log',
|
|
304
|
+
'30 4 * * *',
|
|
305
|
+
'SELECT public.cleanup_event_log()'
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
-- 4a. Webhook retry processing: every 2 minutes
|
|
309
|
+
PERFORM cron.schedule(
|
|
310
|
+
'process-webhook-retries',
|
|
311
|
+
'*/2 * * * *',
|
|
312
|
+
'SELECT public.process_webhook_retries()'
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
-- 4b. Dead-letter + delivered webhook cleanup: daily at 05:00 UTC
|
|
316
|
+
PERFORM cron.schedule(
|
|
317
|
+
'cleanup-dead-webhooks',
|
|
318
|
+
'0 5 * * *',
|
|
319
|
+
'SELECT public.cleanup_dead_webhooks()'
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
-- 5. Usage metrics aggregation: daily at 01:00 UTC
|
|
323
|
+
PERFORM cron.schedule(
|
|
324
|
+
'aggregate-workspace-usage',
|
|
325
|
+
'0 1 * * *',
|
|
326
|
+
'SELECT public.aggregate_workspace_usage()'
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
-- 6. ANALYZE hot tables: daily at 05:30 UTC
|
|
330
|
+
PERFORM cron.schedule(
|
|
331
|
+
'analyze-hot-tables',
|
|
332
|
+
'30 5 * * *',
|
|
333
|
+
'ANALYZE tasks; ANALYZE event_log; ANALYZE request_log; ANALYZE webhook_delivery_log'
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
ELSE
|
|
337
|
+
RAISE NOTICE 'pg_cron not available — skipping production automation schedules';
|
|
338
|
+
END IF;
|
|
339
|
+
END;
|
|
340
|
+
$outer$;
|
|
341
|
+
|
|
342
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
343
|
+
-- RLS for new tables
|
|
344
|
+
-- ══════════════════════════════════════════════════════════════════════
|
|
345
|
+
|
|
346
|
+
ALTER TABLE webhook_delivery_log ENABLE ROW LEVEL SECURITY;
|
|
347
|
+
ALTER TABLE workspace_usage ENABLE ROW LEVEL SECURITY;
|
|
348
|
+
|
|
349
|
+
CREATE POLICY workspace_isolation ON webhook_delivery_log
|
|
350
|
+
FOR ALL USING (workspace_id IN (SELECT get_user_workspace_ids()));
|
|
351
|
+
|
|
352
|
+
CREATE POLICY workspace_isolation ON workspace_usage
|
|
353
|
+
FOR ALL USING (workspace_id IN (SELECT get_user_workspace_ids()));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Fix get_top_tasks: exclude non-actionable statuses (done, cancelled, failed, blocked) by default.
|
|
2
|
+
-- When p_status is explicitly set, filter to that exact status (including non-actionable ones).
|
|
3
|
+
|
|
4
|
+
CREATE OR REPLACE FUNCTION get_top_tasks(
|
|
5
|
+
p_workspace_id uuid, p_project_id uuid,
|
|
6
|
+
p_department_id uuid DEFAULT NULL,
|
|
7
|
+
p_status text DEFAULT NULL,
|
|
8
|
+
p_limit int DEFAULT 10
|
|
9
|
+
) RETURNS SETOF tasks LANGUAGE sql STABLE AS $$
|
|
10
|
+
SELECT * FROM tasks
|
|
11
|
+
WHERE workspace_id = p_workspace_id
|
|
12
|
+
AND project_id = p_project_id
|
|
13
|
+
AND is_archived = false
|
|
14
|
+
AND (p_department_id IS NULL OR department_id = p_department_id)
|
|
15
|
+
AND (p_status IS NOT NULL AND status = p_status::status
|
|
16
|
+
OR p_status IS NULL AND status NOT IN ('done', 'cancelled', 'failed', 'blocked'))
|
|
17
|
+
ORDER BY priority DESC, created_at ASC
|
|
18
|
+
LIMIT LEAST(p_limit, 25);
|
|
19
|
+
$$;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- Rename default_project_id/default_department_id to project_id/department_id
|
|
2
|
+
-- These aren't "defaults" — they're the agent's home project/department.
|
|
3
|
+
|
|
4
|
+
ALTER TABLE agent_keys RENAME COLUMN default_project_id TO project_id;
|
|
5
|
+
ALTER TABLE agent_keys RENAME COLUMN default_department_id TO department_id;
|
|
6
|
+
|
|
7
|
+
-- Rename the CHECK constraint
|
|
8
|
+
ALTER TABLE agent_keys DROP CONSTRAINT agent_keys_default_dept_requires_project;
|
|
9
|
+
ALTER TABLE agent_keys ADD CONSTRAINT agent_keys_dept_requires_project
|
|
10
|
+
CHECK (department_id IS NULL OR project_id IS NOT NULL);
|
|
11
|
+
|
|
12
|
+
-- Recreate trigger function with updated column names
|
|
13
|
+
CREATE OR REPLACE FUNCTION check_workspace_consistency()
|
|
14
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
15
|
+
BEGIN
|
|
16
|
+
IF TG_TABLE_NAME = 'agent_permissions' THEN
|
|
17
|
+
IF NOT EXISTS (SELECT 1 FROM projects WHERE id = NEW.project_id AND workspace_id = NEW.workspace_id) THEN
|
|
18
|
+
RAISE EXCEPTION 'cross-workspace reference: project % not in workspace %', NEW.project_id, NEW.workspace_id;
|
|
19
|
+
END IF;
|
|
20
|
+
IF NEW.department_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM departments WHERE id = NEW.department_id AND workspace_id = NEW.workspace_id) THEN
|
|
21
|
+
RAISE EXCEPTION 'cross-workspace reference: department % not in workspace %', NEW.department_id, NEW.workspace_id;
|
|
22
|
+
END IF;
|
|
23
|
+
END IF;
|
|
24
|
+
|
|
25
|
+
IF TG_TABLE_NAME = 'tasks' THEN
|
|
26
|
+
IF NOT EXISTS (SELECT 1 FROM projects WHERE id = NEW.project_id AND workspace_id = NEW.workspace_id) THEN
|
|
27
|
+
RAISE EXCEPTION 'cross-workspace reference: project % not in workspace %', NEW.project_id, NEW.workspace_id;
|
|
28
|
+
END IF;
|
|
29
|
+
IF NEW.assigned_to_agent_key_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM agent_keys WHERE id = NEW.assigned_to_agent_key_id AND workspace_id = NEW.workspace_id) THEN
|
|
30
|
+
RAISE EXCEPTION 'cross-workspace reference: assignee % not in workspace %', NEW.assigned_to_agent_key_id, NEW.workspace_id;
|
|
31
|
+
END IF;
|
|
32
|
+
END IF;
|
|
33
|
+
|
|
34
|
+
IF TG_TABLE_NAME = 'webhook_subscriptions' THEN
|
|
35
|
+
IF NOT EXISTS (SELECT 1 FROM projects WHERE id = NEW.project_id AND workspace_id = NEW.workspace_id) THEN
|
|
36
|
+
RAISE EXCEPTION 'cross-workspace reference: project % not in workspace %', NEW.project_id, NEW.workspace_id;
|
|
37
|
+
END IF;
|
|
38
|
+
END IF;
|
|
39
|
+
|
|
40
|
+
IF TG_TABLE_NAME = 'agent_keys' THEN
|
|
41
|
+
IF NEW.project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM projects WHERE id = NEW.project_id AND workspace_id = NEW.workspace_id) THEN
|
|
42
|
+
RAISE EXCEPTION 'cross-workspace reference: project % not in workspace %', NEW.project_id, NEW.workspace_id;
|
|
43
|
+
END IF;
|
|
44
|
+
IF NEW.department_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM departments WHERE id = NEW.department_id AND workspace_id = NEW.workspace_id) THEN
|
|
45
|
+
RAISE EXCEPTION 'cross-workspace reference: department % not in workspace %', NEW.department_id, NEW.workspace_id;
|
|
46
|
+
END IF;
|
|
47
|
+
END IF;
|
|
48
|
+
|
|
49
|
+
RETURN NEW;
|
|
50
|
+
END;
|
|
51
|
+
$$;
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "writbase",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "WritBase CLI — self-hosted operator toolkit for agent-first task management",
|
|
6
|
+
"bin": {
|
|
7
|
+
"writbase": "./dist/writbase.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/",
|
|
11
|
+
"migrations/"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup src/bin/writbase.ts --format esm --target node18",
|
|
15
|
+
"postbuild": "rm -rf ./migrations && cp -r ../supabase/migrations ./migrations",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@inquirer/prompts": "^7.0.0",
|
|
23
|
+
"@supabase/supabase-js": "^2.49.1",
|
|
24
|
+
"commander": "^13.1.0",
|
|
25
|
+
"dotenv": "^16.4.7",
|
|
26
|
+
"nanospinner": "^1.2.2",
|
|
27
|
+
"picocolors": "^1.1.1"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"tsup": "^8.4.0",
|
|
32
|
+
"typescript": "^5.7.0"
|
|
33
|
+
}
|
|
34
|
+
}
|