@meyicloud/meyi-insight-server 1.1.6 → 1.1.8

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 (54) hide show
  1. package/dist/constants/incidentCategories.d.ts.map +1 -1
  2. package/dist/constants/incidentCategories.js +2 -1
  3. package/dist/constants/incidentCategories.js.map +1 -1
  4. package/dist/controllers/incident.controller.js +1 -1
  5. package/dist/controllers/incident.controller.js.map +1 -1
  6. package/dist/database/schema.d.ts +17 -0
  7. package/dist/database/schema.d.ts.map +1 -1
  8. package/dist/database/schema.js +2 -0
  9. package/dist/database/schema.js.map +1 -1
  10. package/dist/features/sla/domain/sla.evaluator.d.ts +1 -1
  11. package/dist/features/sla/domain/sla.evaluator.d.ts.map +1 -1
  12. package/dist/features/sla/domain/sla.evaluator.js +3 -2
  13. package/dist/features/sla/domain/sla.evaluator.js.map +1 -1
  14. package/dist/features/sources/cloudtrail/controller.d.ts.map +1 -1
  15. package/dist/features/sources/cloudtrail/controller.js +11 -1
  16. package/dist/features/sources/cloudtrail/controller.js.map +1 -1
  17. package/dist/migrations/run.d.ts.map +1 -1
  18. package/dist/migrations/run.js +1297 -1254
  19. package/dist/migrations/run.js.map +1 -1
  20. package/dist/models/alert.model.d.ts +7 -0
  21. package/dist/models/alert.model.d.ts.map +1 -1
  22. package/dist/models/alert.model.js +28 -1
  23. package/dist/models/alert.model.js.map +1 -1
  24. package/dist/models/incident.model.d.ts.map +1 -1
  25. package/dist/models/incident.model.js +1 -0
  26. package/dist/models/incident.model.js.map +1 -1
  27. package/dist/models/source.model.d.ts.map +1 -1
  28. package/dist/models/source.model.js +21 -7
  29. package/dist/models/source.model.js.map +1 -1
  30. package/dist/pipeline/AlarmPipelineWorker.d.ts.map +1 -1
  31. package/dist/pipeline/AlarmPipelineWorker.js +12 -9
  32. package/dist/pipeline/AlarmPipelineWorker.js.map +1 -1
  33. package/dist/pipeline/NormalizationService.d.ts.map +1 -1
  34. package/dist/pipeline/NormalizationService.js +5 -0
  35. package/dist/pipeline/NormalizationService.js.map +1 -1
  36. package/dist/pipeline/alarmQueue.d.ts +19 -1
  37. package/dist/pipeline/alarmQueue.d.ts.map +1 -1
  38. package/dist/pipeline/alarmQueue.js +93 -16
  39. package/dist/pipeline/alarmQueue.js.map +1 -1
  40. package/dist/routes/index.d.ts.map +1 -1
  41. package/dist/routes/index.js +2 -1
  42. package/dist/routes/index.js.map +1 -1
  43. package/dist/services/alert.service.d.ts +8 -0
  44. package/dist/services/alert.service.d.ts.map +1 -1
  45. package/dist/services/alert.service.js +20 -2
  46. package/dist/services/alert.service.js.map +1 -1
  47. package/dist/services/incident.service.d.ts +17 -1
  48. package/dist/services/incident.service.d.ts.map +1 -1
  49. package/dist/services/incident.service.js +48 -15
  50. package/dist/services/incident.service.js.map +1 -1
  51. package/dist/services/notification-provider.service.d.ts.map +1 -1
  52. package/dist/services/notification-provider.service.js +3 -1
  53. package/dist/services/notification-provider.service.js.map +1 -1
  54. package/package.json +75 -75
@@ -22,146 +22,146 @@ export async function runMigrations(db) {
22
22
  ? defaultTokenTtlDaysRaw
23
23
  : 30;
24
24
  // Rename tables and columns if they exist from the old 'connector' naming convention
25
- await db.execute(sql.raw(`
26
- DO $$
27
- BEGIN
28
- -- Rename main table
29
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_connectors') THEN
30
- IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sources') THEN
31
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_connectors" RENAME TO "insight_sources"';
32
- END IF;
33
- END IF;
34
-
35
- -- Rename CloudWatch setups
36
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudwatch_connector_setups') THEN
37
- IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudwatch_source_setups') THEN
38
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudwatch_connector_setups" RENAME TO "insight_cloudwatch_source_setups"';
39
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudwatch_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
40
- END IF;
41
- END IF;
42
-
43
- -- Rename CloudTrail setups
44
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudtrail_connector_setups') THEN
45
- IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudtrail_source_setups') THEN
46
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudtrail_connector_setups" RENAME TO "insight_cloudtrail_source_setups"';
47
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudtrail_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
48
- END IF;
49
- END IF;
50
-
51
- -- Rename Grafana setups
52
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_grafana_connector_setups') THEN
53
- IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_grafana_source_setups') THEN
54
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_grafana_connector_setups" RENAME TO "insight_grafana_source_setups"';
55
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_grafana_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
56
- END IF;
57
- END IF;
58
-
59
- -- Rename Prometheus setups
60
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_prometheus_connector_setups') THEN
61
- IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_prometheus_source_setups') THEN
62
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_prometheus_connector_setups" RENAME TO "insight_prometheus_source_setups"';
63
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_prometheus_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
64
- END IF;
65
- END IF;
66
-
67
- -- Rename Connector Health
68
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_connector_health') THEN
69
- IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_source_health') THEN
70
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_connector_health" RENAME TO "insight_source_health"';
71
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_source_health" RENAME COLUMN "connector_id" TO "source_id"';
72
- END IF;
73
- END IF;
74
-
75
- -- Rename Webhook Failure Logs
76
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_webhook_failure_logs' AND column_name = 'connector_id') THEN
77
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_webhook_failure_logs" RENAME COLUMN "connector_id" TO "source_id"';
78
- END IF;
79
-
80
- -- Rename columns in alerts
81
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'connector_id') THEN
82
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "connector_id" TO "source_id"';
83
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "connector_name" TO "source_name"';
84
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "connector_type" TO "source_type"';
85
- END IF;
86
-
87
- -- Rename columns in configs
88
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_normalization_configs' AND column_name = 'connector_id') THEN
89
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_normalization_configs" RENAME COLUMN "connector_id" TO "source_id"';
90
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_normalization_configs" RENAME COLUMN "connector_type" TO "source_type"';
91
- END IF;
92
-
93
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_dedup_configs' AND column_name = 'connector_id') THEN
94
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_dedup_configs" RENAME COLUMN "connector_id" TO "source_id"';
95
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_dedup_configs" RENAME COLUMN "connector_type" TO "source_type"';
96
- END IF;
97
-
98
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_grouping_configs' AND column_name = 'connector_id') THEN
99
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_grouping_configs" RENAME COLUMN "connector_id" TO "source_id"';
100
- EXECUTE 'ALTER TABLE "meyiconnect"."insight_grouping_configs" RENAME COLUMN "connector_type" TO "source_type"';
101
- END IF;
102
-
103
- END $$;
104
- `));
105
- await db.execute(sql.raw(`
106
- DO $$
107
- DECLARE
108
- v_relkind "char";
109
- BEGIN
110
- SELECT c.relkind INTO v_relkind
111
- FROM pg_class c
112
- JOIN pg_namespace n ON n.oid = c.relnamespace
113
- WHERE n.nspname = 'meyiconnect' AND c.relname = 'insight_tenants'
114
- LIMIT 1;
115
-
116
- IF v_relkind IS NULL THEN
117
- CREATE TABLE "meyiconnect"."insight_tenants" (
118
- "id" TEXT PRIMARY KEY,
119
- "name" TEXT NOT NULL,
120
- "is_default" BOOLEAN NOT NULL DEFAULT false,
121
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
122
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
123
- );
124
- ${defaultTenantId ? `INSERT INTO "meyiconnect"."insight_tenants" ("id", "name", "is_default")
125
- VALUES ('${defaultTenantSql}', 'Default tenant', true)
126
- ON CONFLICT ("id") DO UPDATE SET "is_default" = true, "updated_at" = NOW();` : "NULL;"}
127
- ELSIF v_relkind = 'r' THEN
128
- ${defaultTenantId ? `INSERT INTO "meyiconnect"."insight_tenants" ("id", "name", "is_default")
129
- VALUES ('${defaultTenantSql}', 'Default tenant', true)
130
- ON CONFLICT ("id") DO UPDATE SET "is_default" = true, "updated_at" = NOW();` : "NULL;"}
131
- END IF;
132
- END $$;
25
+ await db.execute(sql.raw(`
26
+ DO $$
27
+ BEGIN
28
+ -- Rename main table
29
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_connectors') THEN
30
+ IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sources') THEN
31
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_connectors" RENAME TO "insight_sources"';
32
+ END IF;
33
+ END IF;
34
+
35
+ -- Rename CloudWatch setups
36
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudwatch_connector_setups') THEN
37
+ IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudwatch_source_setups') THEN
38
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudwatch_connector_setups" RENAME TO "insight_cloudwatch_source_setups"';
39
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudwatch_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
40
+ END IF;
41
+ END IF;
42
+
43
+ -- Rename CloudTrail setups
44
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudtrail_connector_setups') THEN
45
+ IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_cloudtrail_source_setups') THEN
46
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudtrail_connector_setups" RENAME TO "insight_cloudtrail_source_setups"';
47
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_cloudtrail_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
48
+ END IF;
49
+ END IF;
50
+
51
+ -- Rename Grafana setups
52
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_grafana_connector_setups') THEN
53
+ IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_grafana_source_setups') THEN
54
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_grafana_connector_setups" RENAME TO "insight_grafana_source_setups"';
55
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_grafana_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
56
+ END IF;
57
+ END IF;
58
+
59
+ -- Rename Prometheus setups
60
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_prometheus_connector_setups') THEN
61
+ IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_prometheus_source_setups') THEN
62
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_prometheus_connector_setups" RENAME TO "insight_prometheus_source_setups"';
63
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_prometheus_source_setups" RENAME COLUMN "connector_id" TO "source_id"';
64
+ END IF;
65
+ END IF;
66
+
67
+ -- Rename Connector Health
68
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_connector_health') THEN
69
+ IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'insight_source_health') THEN
70
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_connector_health" RENAME TO "insight_source_health"';
71
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_source_health" RENAME COLUMN "connector_id" TO "source_id"';
72
+ END IF;
73
+ END IF;
74
+
75
+ -- Rename Webhook Failure Logs
76
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_webhook_failure_logs' AND column_name = 'connector_id') THEN
77
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_webhook_failure_logs" RENAME COLUMN "connector_id" TO "source_id"';
78
+ END IF;
79
+
80
+ -- Rename columns in alerts
81
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'connector_id') THEN
82
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "connector_id" TO "source_id"';
83
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "connector_name" TO "source_name"';
84
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "connector_type" TO "source_type"';
85
+ END IF;
86
+
87
+ -- Rename columns in configs
88
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_normalization_configs' AND column_name = 'connector_id') THEN
89
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_normalization_configs" RENAME COLUMN "connector_id" TO "source_id"';
90
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_normalization_configs" RENAME COLUMN "connector_type" TO "source_type"';
91
+ END IF;
92
+
93
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_dedup_configs' AND column_name = 'connector_id') THEN
94
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_dedup_configs" RENAME COLUMN "connector_id" TO "source_id"';
95
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_dedup_configs" RENAME COLUMN "connector_type" TO "source_type"';
96
+ END IF;
97
+
98
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_grouping_configs' AND column_name = 'connector_id') THEN
99
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_grouping_configs" RENAME COLUMN "connector_id" TO "source_id"';
100
+ EXECUTE 'ALTER TABLE "meyiconnect"."insight_grouping_configs" RENAME COLUMN "connector_type" TO "source_type"';
101
+ END IF;
102
+
103
+ END $$;
104
+ `));
105
+ await db.execute(sql.raw(`
106
+ DO $$
107
+ DECLARE
108
+ v_relkind "char";
109
+ BEGIN
110
+ SELECT c.relkind INTO v_relkind
111
+ FROM pg_class c
112
+ JOIN pg_namespace n ON n.oid = c.relnamespace
113
+ WHERE n.nspname = 'meyiconnect' AND c.relname = 'insight_tenants'
114
+ LIMIT 1;
115
+
116
+ IF v_relkind IS NULL THEN
117
+ CREATE TABLE "meyiconnect"."insight_tenants" (
118
+ "id" TEXT PRIMARY KEY,
119
+ "name" TEXT NOT NULL,
120
+ "is_default" BOOLEAN NOT NULL DEFAULT false,
121
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
122
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
123
+ );
124
+ ${defaultTenantId ? `INSERT INTO "meyiconnect"."insight_tenants" ("id", "name", "is_default")
125
+ VALUES ('${defaultTenantSql}', 'Default tenant', true)
126
+ ON CONFLICT ("id") DO UPDATE SET "is_default" = true, "updated_at" = NOW();` : "NULL;"}
127
+ ELSIF v_relkind = 'r' THEN
128
+ ${defaultTenantId ? `INSERT INTO "meyiconnect"."insight_tenants" ("id", "name", "is_default")
129
+ VALUES ('${defaultTenantSql}', 'Default tenant', true)
130
+ ON CONFLICT ("id") DO UPDATE SET "is_default" = true, "updated_at" = NOW();` : "NULL;"}
131
+ END IF;
132
+ END $$;
133
133
  `));
134
134
  // insight_sources
135
- await db.execute(sql.raw(`
136
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_sources" (
137
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
138
- "name" TEXT NOT NULL,
139
- "type" TEXT NOT NULL,
140
- "webhook_token" TEXT NOT NULL,
141
- "webhook_auth_token" TEXT,
142
- "webhook_token_never_expires" BOOLEAN NOT NULL DEFAULT false,
143
- "webhook_token_expires_at" TIMESTAMPTZ,
144
- "is_active" BOOLEAN NOT NULL DEFAULT true,
145
- "description" TEXT,
146
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
147
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
148
- )
149
- `));
150
- await db.execute(sql.raw(`
151
- DO $$
152
- BEGIN
153
- IF EXISTS (
154
- SELECT 1 FROM information_schema.columns
155
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sources' AND column_name = 'tenant_id' AND data_type = 'uuid'
156
- ) THEN
157
- ALTER TABLE "meyiconnect"."insight_sources" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
158
- ELSIF NOT EXISTS (
159
- SELECT 1 FROM information_schema.columns
160
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sources' AND column_name = 'tenant_id'
161
- ) THEN
162
- ALTER TABLE "meyiconnect"."insight_sources" ADD COLUMN "tenant_id" TEXT;
163
- END IF;
164
- END $$;
135
+ await db.execute(sql.raw(`
136
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_sources" (
137
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
138
+ "name" TEXT NOT NULL,
139
+ "type" TEXT NOT NULL,
140
+ "webhook_token" TEXT NOT NULL,
141
+ "webhook_auth_token" TEXT,
142
+ "webhook_token_never_expires" BOOLEAN NOT NULL DEFAULT false,
143
+ "webhook_token_expires_at" TIMESTAMPTZ,
144
+ "is_active" BOOLEAN NOT NULL DEFAULT true,
145
+ "description" TEXT,
146
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
147
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
148
+ )
149
+ `));
150
+ await db.execute(sql.raw(`
151
+ DO $$
152
+ BEGIN
153
+ IF EXISTS (
154
+ SELECT 1 FROM information_schema.columns
155
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sources' AND column_name = 'tenant_id' AND data_type = 'uuid'
156
+ ) THEN
157
+ ALTER TABLE "meyiconnect"."insight_sources" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
158
+ ELSIF NOT EXISTS (
159
+ SELECT 1 FROM information_schema.columns
160
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sources' AND column_name = 'tenant_id'
161
+ ) THEN
162
+ ALTER TABLE "meyiconnect"."insight_sources" ADD COLUMN "tenant_id" TEXT;
163
+ END IF;
164
+ END $$;
165
165
  `));
166
166
  await backfill(`UPDATE "meyiconnect"."insight_sources" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id"::text = ''`);
167
167
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_sources_tenant_id_idx ON "meyiconnect"."insight_sources" ("tenant_id")`));
@@ -171,63 +171,63 @@ export async function runMigrations(db) {
171
171
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sources" ADD COLUMN IF NOT EXISTS "description" TEXT`));
172
172
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sources" ALTER COLUMN "webhook_token_expires_at" SET DEFAULT (NOW() + INTERVAL '${defaultTokenTtlDays} days')`));
173
173
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sources" DROP COLUMN IF EXISTS "config"`));
174
- await db.execute(sql.raw(`
175
- UPDATE "meyiconnect"."insight_sources"
176
- SET "webhook_token" = gen_random_uuid()::TEXT
177
- WHERE "webhook_token" IS NULL
174
+ await db.execute(sql.raw(`
175
+ UPDATE "meyiconnect"."insight_sources"
176
+ SET "webhook_token" = gen_random_uuid()::TEXT
177
+ WHERE "webhook_token" IS NULL
178
178
  `));
179
179
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sources" ALTER COLUMN "webhook_token" SET NOT NULL`)).catch(() => { });
180
- await db.execute(sql.raw(`
181
- UPDATE "meyiconnect"."insight_sources"
182
- SET "webhook_token_expires_at" = NOW() + INTERVAL '${defaultTokenTtlDays} days'
183
- WHERE COALESCE("webhook_token_never_expires", FALSE) = FALSE
184
- AND "webhook_token_expires_at" IS NULL
180
+ await db.execute(sql.raw(`
181
+ UPDATE "meyiconnect"."insight_sources"
182
+ SET "webhook_token_expires_at" = NOW() + INTERVAL '${defaultTokenTtlDays} days'
183
+ WHERE COALESCE("webhook_token_never_expires", FALSE) = FALSE
184
+ AND "webhook_token_expires_at" IS NULL
185
185
  `));
186
- await db.execute(sql.raw(`
187
- CREATE UNIQUE INDEX IF NOT EXISTS insight_sources_webhook_token_unique
188
- ON "meyiconnect"."insight_sources" ("webhook_token")
186
+ await db.execute(sql.raw(`
187
+ CREATE UNIQUE INDEX IF NOT EXISTS insight_sources_webhook_token_unique
188
+ ON "meyiconnect"."insight_sources" ("webhook_token")
189
189
  `));
190
190
  // CloudWatch source setup lives in a dedicated one-to-one table.
191
- await db.execute(sql.raw(`
192
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_cloudwatch_source_setups" (
193
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
194
- "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
195
- "aws_access_key_id" TEXT,
196
- "aws_secret_access_key" TEXT,
197
- "aws_session_token" TEXT,
198
- "aws_region" TEXT,
199
- "aws_stack_name" TEXT,
200
- "aws_lambda_name" TEXT,
201
- "aws_alarm_name_prefix" TEXT,
202
- "aws_selected_alarm_names" JSONB DEFAULT '[]',
203
- "aws_selected_metric_keys" JSONB DEFAULT '[]',
204
- "aws_deployment_status" TEXT NOT NULL DEFAULT 'not_started',
205
- "aws_deployment_status_reason" TEXT,
206
- "aws_stack_id" TEXT,
207
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
208
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
209
- )
191
+ await db.execute(sql.raw(`
192
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_cloudwatch_source_setups" (
193
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
194
+ "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
195
+ "aws_access_key_id" TEXT,
196
+ "aws_secret_access_key" TEXT,
197
+ "aws_session_token" TEXT,
198
+ "aws_region" TEXT,
199
+ "aws_stack_name" TEXT,
200
+ "aws_lambda_name" TEXT,
201
+ "aws_alarm_name_prefix" TEXT,
202
+ "aws_selected_alarm_names" JSONB DEFAULT '[]',
203
+ "aws_selected_metric_keys" JSONB DEFAULT '[]',
204
+ "aws_deployment_status" TEXT NOT NULL DEFAULT 'not_started',
205
+ "aws_deployment_status_reason" TEXT,
206
+ "aws_stack_id" TEXT,
207
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
208
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
209
+ )
210
210
  `));
211
211
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS insight_cloudwatch_source_setups_source_unique ON "meyiconnect"."insight_cloudwatch_source_setups" ("source_id")`));
212
212
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_cloudwatch_source_setups_status_idx ON "meyiconnect"."insight_cloudwatch_source_setups" ("aws_deployment_status")`));
213
213
  // CloudTrail source setup — dedicated one-to-one side-table (same pattern as cloudwatch).
214
- await db.execute(sql.raw(`
215
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_cloudtrail_source_setups" (
216
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
217
- "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
218
- "aws_region" TEXT,
219
- "aws_stack_name" TEXT,
220
- "aws_create_new_trail" BOOLEAN NOT NULL DEFAULT true,
221
- "aws_deployment_status" TEXT NOT NULL DEFAULT 'not_started',
222
- "aws_deployment_status_reason" TEXT,
223
- "aws_stack_id" TEXT,
224
- "aws_deployment_mode" TEXT DEFAULT 'Organizations',
225
- "aws_target_ous" TEXT,
226
- "aws_target_accounts" TEXT,
227
- "aws_target_regions" TEXT,
228
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
229
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
230
- )
214
+ await db.execute(sql.raw(`
215
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_cloudtrail_source_setups" (
216
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
217
+ "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
218
+ "aws_region" TEXT,
219
+ "aws_stack_name" TEXT,
220
+ "aws_create_new_trail" BOOLEAN NOT NULL DEFAULT true,
221
+ "aws_deployment_status" TEXT NOT NULL DEFAULT 'not_started',
222
+ "aws_deployment_status_reason" TEXT,
223
+ "aws_stack_id" TEXT,
224
+ "aws_deployment_mode" TEXT DEFAULT 'Organizations',
225
+ "aws_target_ous" TEXT,
226
+ "aws_target_accounts" TEXT,
227
+ "aws_target_regions" TEXT,
228
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
229
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
230
+ )
231
231
  `));
232
232
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS insight_cloudtrail_source_setups_source_unique ON "meyiconnect"."insight_cloudtrail_source_setups" ("source_id")`));
233
233
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_cloudtrail_source_setups_status_idx ON "meyiconnect"."insight_cloudtrail_source_setups" ("aws_deployment_status")`));
@@ -236,209 +236,209 @@ export async function runMigrations(db) {
236
236
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_cloudtrail_source_setups" ADD COLUMN IF NOT EXISTS "aws_target_ous" TEXT`));
237
237
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_cloudtrail_source_setups" ADD COLUMN IF NOT EXISTS "aws_target_accounts" TEXT`));
238
238
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_cloudtrail_source_setups" ADD COLUMN IF NOT EXISTS "aws_target_regions" TEXT`));
239
- await db.execute(sql.raw(`
240
- DO $$
241
- BEGIN
242
- IF EXISTS (
243
- SELECT 1
244
- FROM information_schema.columns
245
- WHERE table_schema = 'meyiconnect'
246
- AND table_name = 'insight_sources'
247
- AND column_name = 'aws_region'
248
- ) THEN
249
- INSERT INTO "meyiconnect"."insight_cloudwatch_source_setups" (
250
- "source_id",
251
- "aws_access_key_id",
252
- "aws_secret_access_key",
253
- "aws_session_token",
254
- "aws_region",
255
- "aws_stack_name",
256
- "aws_lambda_name",
257
- "aws_alarm_name_prefix",
258
- "aws_selected_alarm_names",
259
- "aws_selected_metric_keys",
260
- "aws_deployment_status",
261
- "aws_deployment_status_reason",
262
- "aws_stack_id",
263
- "created_at",
264
- "updated_at"
265
- )
266
- SELECT
267
- "id",
268
- "aws_access_key_id",
269
- "aws_secret_access_key",
270
- "aws_session_token",
271
- "aws_region",
272
- "aws_stack_name",
273
- "aws_lambda_name",
274
- "aws_alarm_name_prefix",
275
- COALESCE("aws_selected_alarm_names", '[]'::jsonb),
276
- COALESCE("aws_selected_metric_keys", '[]'::jsonb),
277
- COALESCE("aws_deployment_status", 'not_started'),
278
- "aws_deployment_status_reason",
279
- "aws_stack_id",
280
- NOW(),
281
- NOW()
282
- FROM "meyiconnect"."insight_sources"
283
- WHERE "type" = 'cloudwatch'
284
- AND (
285
- "aws_access_key_id" IS NOT NULL OR
286
- "aws_secret_access_key" IS NOT NULL OR
287
- "aws_session_token" IS NOT NULL OR
288
- "aws_region" IS NOT NULL OR
289
- "aws_stack_name" IS NOT NULL OR
290
- "aws_lambda_name" IS NOT NULL OR
291
- "aws_alarm_name_prefix" IS NOT NULL OR
292
- "aws_deployment_status" IS NOT NULL OR
293
- "aws_stack_id" IS NOT NULL
294
- )
295
- ON CONFLICT ("source_id") DO UPDATE SET
296
- "aws_access_key_id" = EXCLUDED."aws_access_key_id",
297
- "aws_secret_access_key" = EXCLUDED."aws_secret_access_key",
298
- "aws_session_token" = EXCLUDED."aws_session_token",
299
- "aws_region" = EXCLUDED."aws_region",
300
- "aws_stack_name" = EXCLUDED."aws_stack_name",
301
- "aws_lambda_name" = EXCLUDED."aws_lambda_name",
302
- "aws_alarm_name_prefix" = EXCLUDED."aws_alarm_name_prefix",
303
- "aws_selected_alarm_names" = EXCLUDED."aws_selected_alarm_names",
304
- "aws_selected_metric_keys" = EXCLUDED."aws_selected_metric_keys",
305
- "aws_deployment_status" = EXCLUDED."aws_deployment_status",
306
- "aws_deployment_status_reason" = EXCLUDED."aws_deployment_status_reason",
307
- "aws_stack_id" = EXCLUDED."aws_stack_id",
308
- "updated_at" = NOW();
309
- END IF;
310
- END $$;
311
- `));
312
- await db.execute(sql.raw(`
313
- ALTER TABLE "meyiconnect"."insight_sources"
314
- DROP COLUMN IF EXISTS "aws_access_key_id",
315
- DROP COLUMN IF EXISTS "aws_secret_access_key",
316
- DROP COLUMN IF EXISTS "aws_session_token",
317
- DROP COLUMN IF EXISTS "aws_region",
318
- DROP COLUMN IF EXISTS "aws_stack_name",
319
- DROP COLUMN IF EXISTS "aws_lambda_name",
320
- DROP COLUMN IF EXISTS "aws_alarm_name_prefix",
321
- DROP COLUMN IF EXISTS "aws_selected_alarm_names",
322
- DROP COLUMN IF EXISTS "aws_selected_metric_keys",
323
- DROP COLUMN IF EXISTS "aws_deployment_status",
324
- DROP COLUMN IF EXISTS "aws_deployment_status_reason",
325
- DROP COLUMN IF EXISTS "aws_stack_id"
239
+ await db.execute(sql.raw(`
240
+ DO $$
241
+ BEGIN
242
+ IF EXISTS (
243
+ SELECT 1
244
+ FROM information_schema.columns
245
+ WHERE table_schema = 'meyiconnect'
246
+ AND table_name = 'insight_sources'
247
+ AND column_name = 'aws_region'
248
+ ) THEN
249
+ INSERT INTO "meyiconnect"."insight_cloudwatch_source_setups" (
250
+ "source_id",
251
+ "aws_access_key_id",
252
+ "aws_secret_access_key",
253
+ "aws_session_token",
254
+ "aws_region",
255
+ "aws_stack_name",
256
+ "aws_lambda_name",
257
+ "aws_alarm_name_prefix",
258
+ "aws_selected_alarm_names",
259
+ "aws_selected_metric_keys",
260
+ "aws_deployment_status",
261
+ "aws_deployment_status_reason",
262
+ "aws_stack_id",
263
+ "created_at",
264
+ "updated_at"
265
+ )
266
+ SELECT
267
+ "id",
268
+ "aws_access_key_id",
269
+ "aws_secret_access_key",
270
+ "aws_session_token",
271
+ "aws_region",
272
+ "aws_stack_name",
273
+ "aws_lambda_name",
274
+ "aws_alarm_name_prefix",
275
+ COALESCE("aws_selected_alarm_names", '[]'::jsonb),
276
+ COALESCE("aws_selected_metric_keys", '[]'::jsonb),
277
+ COALESCE("aws_deployment_status", 'not_started'),
278
+ "aws_deployment_status_reason",
279
+ "aws_stack_id",
280
+ NOW(),
281
+ NOW()
282
+ FROM "meyiconnect"."insight_sources"
283
+ WHERE "type" = 'cloudwatch'
284
+ AND (
285
+ "aws_access_key_id" IS NOT NULL OR
286
+ "aws_secret_access_key" IS NOT NULL OR
287
+ "aws_session_token" IS NOT NULL OR
288
+ "aws_region" IS NOT NULL OR
289
+ "aws_stack_name" IS NOT NULL OR
290
+ "aws_lambda_name" IS NOT NULL OR
291
+ "aws_alarm_name_prefix" IS NOT NULL OR
292
+ "aws_deployment_status" IS NOT NULL OR
293
+ "aws_stack_id" IS NOT NULL
294
+ )
295
+ ON CONFLICT ("source_id") DO UPDATE SET
296
+ "aws_access_key_id" = EXCLUDED."aws_access_key_id",
297
+ "aws_secret_access_key" = EXCLUDED."aws_secret_access_key",
298
+ "aws_session_token" = EXCLUDED."aws_session_token",
299
+ "aws_region" = EXCLUDED."aws_region",
300
+ "aws_stack_name" = EXCLUDED."aws_stack_name",
301
+ "aws_lambda_name" = EXCLUDED."aws_lambda_name",
302
+ "aws_alarm_name_prefix" = EXCLUDED."aws_alarm_name_prefix",
303
+ "aws_selected_alarm_names" = EXCLUDED."aws_selected_alarm_names",
304
+ "aws_selected_metric_keys" = EXCLUDED."aws_selected_metric_keys",
305
+ "aws_deployment_status" = EXCLUDED."aws_deployment_status",
306
+ "aws_deployment_status_reason" = EXCLUDED."aws_deployment_status_reason",
307
+ "aws_stack_id" = EXCLUDED."aws_stack_id",
308
+ "updated_at" = NOW();
309
+ END IF;
310
+ END $$;
311
+ `));
312
+ await db.execute(sql.raw(`
313
+ ALTER TABLE "meyiconnect"."insight_sources"
314
+ DROP COLUMN IF EXISTS "aws_access_key_id",
315
+ DROP COLUMN IF EXISTS "aws_secret_access_key",
316
+ DROP COLUMN IF EXISTS "aws_session_token",
317
+ DROP COLUMN IF EXISTS "aws_region",
318
+ DROP COLUMN IF EXISTS "aws_stack_name",
319
+ DROP COLUMN IF EXISTS "aws_lambda_name",
320
+ DROP COLUMN IF EXISTS "aws_alarm_name_prefix",
321
+ DROP COLUMN IF EXISTS "aws_selected_alarm_names",
322
+ DROP COLUMN IF EXISTS "aws_selected_metric_keys",
323
+ DROP COLUMN IF EXISTS "aws_deployment_status",
324
+ DROP COLUMN IF EXISTS "aws_deployment_status_reason",
325
+ DROP COLUMN IF EXISTS "aws_stack_id"
326
326
  `));
327
327
  // Grafana source setup lives in a dedicated one-to-one table.
328
- await db.execute(sql.raw(`
329
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_grafana_source_setups" (
330
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
331
- "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
332
- "grafana_base_url" TEXT,
333
- "grafana_contact_point_name" TEXT,
334
- "grafana_account_token_encrypted" TEXT,
335
- "grafana_auto_route" BOOLEAN NOT NULL DEFAULT false,
336
- "grafana_matcher_label" TEXT,
337
- "grafana_matcher_value" TEXT,
338
- "grafana_policy_updated" BOOLEAN NOT NULL DEFAULT false,
339
- "grafana_setup_status" TEXT NOT NULL DEFAULT 'not_started',
340
- "grafana_setup_status_reason" TEXT,
341
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
342
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
343
- )
328
+ await db.execute(sql.raw(`
329
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_grafana_source_setups" (
330
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
331
+ "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
332
+ "grafana_base_url" TEXT,
333
+ "grafana_contact_point_name" TEXT,
334
+ "grafana_account_token_encrypted" TEXT,
335
+ "grafana_auto_route" BOOLEAN NOT NULL DEFAULT false,
336
+ "grafana_matcher_label" TEXT,
337
+ "grafana_matcher_value" TEXT,
338
+ "grafana_policy_updated" BOOLEAN NOT NULL DEFAULT false,
339
+ "grafana_setup_status" TEXT NOT NULL DEFAULT 'not_started',
340
+ "grafana_setup_status_reason" TEXT,
341
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
342
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
343
+ )
344
344
  `));
345
345
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_grafana_source_setups" ADD COLUMN IF NOT EXISTS "grafana_account_token_encrypted" TEXT`));
346
346
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS insight_grafana_source_setups_source_unique ON "meyiconnect"."insight_grafana_source_setups" ("source_id")`));
347
347
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_grafana_source_setups_status_idx ON "meyiconnect"."insight_grafana_source_setups" ("grafana_setup_status")`));
348
348
  // Prometheus source setup lives in a dedicated one-to-one table.
349
- await db.execute(sql.raw(`
350
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_prometheus_source_setups" (
351
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
352
- "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
353
- "prometheus_environment" TEXT,
354
- "prometheus_receiver_name" TEXT,
355
- "prometheus_base_url" TEXT,
356
- "alertmanager_base_url" TEXT,
357
- "prometheus_setup_status" TEXT NOT NULL DEFAULT 'not_started',
358
- "prometheus_setup_status_reason" TEXT,
359
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
360
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
361
- )
349
+ await db.execute(sql.raw(`
350
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_prometheus_source_setups" (
351
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
352
+ "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
353
+ "prometheus_environment" TEXT,
354
+ "prometheus_receiver_name" TEXT,
355
+ "prometheus_base_url" TEXT,
356
+ "alertmanager_base_url" TEXT,
357
+ "prometheus_setup_status" TEXT NOT NULL DEFAULT 'not_started',
358
+ "prometheus_setup_status_reason" TEXT,
359
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
360
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
361
+ )
362
362
  `));
363
363
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_prometheus_source_setups" ADD COLUMN IF NOT EXISTS "prometheus_base_url" TEXT`));
364
364
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_prometheus_source_setups" ADD COLUMN IF NOT EXISTS "alertmanager_base_url" TEXT`));
365
365
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS insight_prometheus_source_setups_source_unique ON "meyiconnect"."insight_prometheus_source_setups" ("source_id")`));
366
366
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_prometheus_source_setups_status_idx ON "meyiconnect"."insight_prometheus_source_setups" ("prometheus_setup_status")`));
367
367
  // Source observability: one health row per source + failed webhook log.
368
- await db.execute(sql.raw(`
369
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_source_health" (
370
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
371
- "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
372
- "delivery_status" TEXT NOT NULL DEFAULT 'unknown',
373
- "last_webhook_received_at" TIMESTAMPTZ,
374
- "last_successful_webhook_at" TIMESTAMPTZ,
375
- "last_failed_webhook_at" TIMESTAMPTZ,
376
- "last_error_code" TEXT,
377
- "last_error_message" TEXT,
378
- "last_alert_id" UUID,
379
- "success_count" INTEGER NOT NULL DEFAULT 0,
380
- "failure_count" INTEGER NOT NULL DEFAULT 0,
381
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
382
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
383
- )
368
+ await db.execute(sql.raw(`
369
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_source_health" (
370
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
371
+ "source_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
372
+ "delivery_status" TEXT NOT NULL DEFAULT 'unknown',
373
+ "last_webhook_received_at" TIMESTAMPTZ,
374
+ "last_successful_webhook_at" TIMESTAMPTZ,
375
+ "last_failed_webhook_at" TIMESTAMPTZ,
376
+ "last_error_code" TEXT,
377
+ "last_error_message" TEXT,
378
+ "last_alert_id" UUID,
379
+ "success_count" INTEGER NOT NULL DEFAULT 0,
380
+ "failure_count" INTEGER NOT NULL DEFAULT 0,
381
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
382
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
383
+ )
384
384
  `));
385
385
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS insight_source_health_source_unique ON "meyiconnect"."insight_source_health" ("source_id")`));
386
386
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_source_health_status_idx ON "meyiconnect"."insight_source_health" ("delivery_status")`));
387
- await db.execute(sql.raw(`
388
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_webhook_failure_logs" (
389
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
390
- "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE SET NULL,
391
- "token_hash" TEXT,
392
- "token_preview" TEXT,
393
- "error_code" TEXT NOT NULL,
394
- "error_message" TEXT NOT NULL,
395
- "request_ip" TEXT,
396
- "user_agent" TEXT,
397
- "method" TEXT,
398
- "path" TEXT,
399
- "payload" JSONB,
400
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
401
- )
387
+ await db.execute(sql.raw(`
388
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_webhook_failure_logs" (
389
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
390
+ "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE SET NULL,
391
+ "token_hash" TEXT,
392
+ "token_preview" TEXT,
393
+ "error_code" TEXT NOT NULL,
394
+ "error_message" TEXT NOT NULL,
395
+ "request_ip" TEXT,
396
+ "user_agent" TEXT,
397
+ "method" TEXT,
398
+ "path" TEXT,
399
+ "payload" JSONB,
400
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
401
+ )
402
402
  `));
403
403
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_webhook_failure_logs_source_idx ON "meyiconnect"."insight_webhook_failure_logs" ("source_id")`));
404
404
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_webhook_failure_logs_error_idx ON "meyiconnect"."insight_webhook_failure_logs" ("error_code")`));
405
405
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_webhook_failure_logs_created_idx ON "meyiconnect"."insight_webhook_failure_logs" ("created_at")`));
406
406
  // insight_projects
407
- await db.execute(sql.raw(`
408
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_projects" (
409
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
410
- "name" TEXT NOT NULL,
411
- "tag" TEXT NOT NULL,
412
- "description" TEXT,
413
- "dependencies" JSONB DEFAULT '[]',
414
- "aws_access_key_id" TEXT,
415
- "aws_secret_access_key" TEXT,
416
- "aws_region" TEXT,
417
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
418
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
419
- )
420
- `));
421
- await db.execute(sql.raw(`
422
- DO $$
423
- BEGIN
424
- IF EXISTS (
425
- SELECT 1 FROM information_schema.columns
426
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_projects' AND column_name = 'tenant_id' AND data_type = 'uuid'
427
- ) THEN
428
- ALTER TABLE "meyiconnect"."insight_projects" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
429
- ELSIF NOT EXISTS (
430
- SELECT 1 FROM information_schema.columns
431
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_projects' AND column_name = 'tenant_id'
432
- ) THEN
433
- ALTER TABLE "meyiconnect"."insight_projects" ADD COLUMN "tenant_id" TEXT;
434
- END IF;
435
- END $$;
436
- `));
437
- await db.execute(sql.raw(`
438
- ALTER TABLE "meyiconnect"."insight_projects"
439
- ADD COLUMN IF NOT EXISTS "aws_access_key_id" TEXT,
440
- ADD COLUMN IF NOT EXISTS "aws_secret_access_key" TEXT,
441
- ADD COLUMN IF NOT EXISTS "aws_region" TEXT
407
+ await db.execute(sql.raw(`
408
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_projects" (
409
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
410
+ "name" TEXT NOT NULL,
411
+ "tag" TEXT NOT NULL,
412
+ "description" TEXT,
413
+ "dependencies" JSONB DEFAULT '[]',
414
+ "aws_access_key_id" TEXT,
415
+ "aws_secret_access_key" TEXT,
416
+ "aws_region" TEXT,
417
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
418
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
419
+ )
420
+ `));
421
+ await db.execute(sql.raw(`
422
+ DO $$
423
+ BEGIN
424
+ IF EXISTS (
425
+ SELECT 1 FROM information_schema.columns
426
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_projects' AND column_name = 'tenant_id' AND data_type = 'uuid'
427
+ ) THEN
428
+ ALTER TABLE "meyiconnect"."insight_projects" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
429
+ ELSIF NOT EXISTS (
430
+ SELECT 1 FROM information_schema.columns
431
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_projects' AND column_name = 'tenant_id'
432
+ ) THEN
433
+ ALTER TABLE "meyiconnect"."insight_projects" ADD COLUMN "tenant_id" TEXT;
434
+ END IF;
435
+ END $$;
436
+ `));
437
+ await db.execute(sql.raw(`
438
+ ALTER TABLE "meyiconnect"."insight_projects"
439
+ ADD COLUMN IF NOT EXISTS "aws_access_key_id" TEXT,
440
+ ADD COLUMN IF NOT EXISTS "aws_secret_access_key" TEXT,
441
+ ADD COLUMN IF NOT EXISTS "aws_region" TEXT
442
442
  `));
443
443
  await backfill(`UPDATE "meyiconnect"."insight_projects" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id"::text = ''`);
444
444
  await db.execute(sql.raw(`DROP INDEX IF EXISTS "meyiconnect"."insight_projects_tag_unique"`)).catch(() => { });
@@ -447,54 +447,54 @@ export async function runMigrations(db) {
447
447
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_projects_tag_idx ON "meyiconnect"."insight_projects" ("tag")`));
448
448
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_projects_tenant_id_idx ON "meyiconnect"."insight_projects" ("tenant_id")`));
449
449
  // insight_incidents
450
- await db.execute(sql.raw(`
451
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_incidents" (
452
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
453
- "alias_id" SERIAL,
454
- "monitor_id" TEXT,
455
- "monitor_name" TEXT,
456
- "monitor_type" TEXT,
457
- "monitor_url" TEXT,
458
- "name" TEXT NOT NULL DEFAULT '',
459
- "source" TEXT NOT NULL DEFAULT '',
460
- "resource_name" TEXT NOT NULL DEFAULT '',
461
- "resource_type" TEXT NOT NULL DEFAULT '',
462
- "down_message" TEXT,
463
- "project_tag" TEXT,
464
- "down_time" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
465
- "incident_down_time" TEXT NOT NULL DEFAULT '',
466
- "resolved_at" TIMESTAMPTZ,
467
- "analysis_status" TEXT NOT NULL DEFAULT 'pending',
468
- "analysis_percent" INTEGER NOT NULL DEFAULT 0,
469
- "analysis_triggered_at" TIMESTAMPTZ,
470
- "analysis_completed_at" TIMESTAMPTZ,
471
- "analysis_result" JSONB,
472
- "remediation_steps" JSONB DEFAULT '[]',
473
- "confidence_score" REAL,
474
- "ai_model_used" TEXT,
475
- "source_alert_id" UUID,
476
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
477
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
478
- )
450
+ await db.execute(sql.raw(`
451
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_incidents" (
452
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
453
+ "alias_id" SERIAL,
454
+ "monitor_id" TEXT,
455
+ "monitor_name" TEXT,
456
+ "monitor_type" TEXT,
457
+ "monitor_url" TEXT,
458
+ "name" TEXT NOT NULL DEFAULT '',
459
+ "source" TEXT NOT NULL DEFAULT '',
460
+ "resource_name" TEXT NOT NULL DEFAULT '',
461
+ "resource_type" TEXT NOT NULL DEFAULT '',
462
+ "down_message" TEXT,
463
+ "project_tag" TEXT,
464
+ "down_time" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
465
+ "incident_down_time" TEXT NOT NULL DEFAULT '',
466
+ "resolved_at" TIMESTAMPTZ,
467
+ "analysis_status" TEXT NOT NULL DEFAULT 'pending',
468
+ "analysis_percent" INTEGER NOT NULL DEFAULT 0,
469
+ "analysis_triggered_at" TIMESTAMPTZ,
470
+ "analysis_completed_at" TIMESTAMPTZ,
471
+ "analysis_result" JSONB,
472
+ "remediation_steps" JSONB DEFAULT '[]',
473
+ "confidence_score" REAL,
474
+ "ai_model_used" TEXT,
475
+ "source_alert_id" UUID,
476
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
477
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
478
+ )
479
479
  `));
480
480
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_incidents_monitor_id_idx ON "meyiconnect"."insight_incidents" ("monitor_id")`));
481
481
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_incidents_status_idx ON "meyiconnect"."insight_incidents" ("analysis_status")`));
482
482
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "alias_id" SERIAL`));
483
- await db.execute(sql.raw(`
484
- DO $$
485
- BEGIN
486
- IF EXISTS (
487
- SELECT 1 FROM information_schema.columns
488
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'tenant_id' AND data_type = 'uuid'
489
- ) THEN
490
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
491
- ELSIF NOT EXISTS (
492
- SELECT 1 FROM information_schema.columns
493
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'tenant_id'
494
- ) THEN
495
- ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN "tenant_id" TEXT;
496
- END IF;
497
- END $$;
483
+ await db.execute(sql.raw(`
484
+ DO $$
485
+ BEGIN
486
+ IF EXISTS (
487
+ SELECT 1 FROM information_schema.columns
488
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'tenant_id' AND data_type = 'uuid'
489
+ ) THEN
490
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
491
+ ELSIF NOT EXISTS (
492
+ SELECT 1 FROM information_schema.columns
493
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'tenant_id'
494
+ ) THEN
495
+ ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN "tenant_id" TEXT;
496
+ END IF;
497
+ END $$;
498
498
  `));
499
499
  await backfill(`UPDATE "meyiconnect"."insight_incidents" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id"::text = ''`);
500
500
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_incidents_tenant_id_idx ON "meyiconnect"."insight_incidents" ("tenant_id")`));
@@ -504,28 +504,28 @@ export async function runMigrations(db) {
504
504
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "resource_name" TEXT`));
505
505
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "resource_type" TEXT`));
506
506
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "down_time" TIMESTAMPTZ`));
507
- await db.execute(sql.raw(`
508
- DO $$
509
- BEGIN
510
- UPDATE "meyiconnect"."insight_incidents" SET "name" = COALESCE("monitor_name", 'Unknown') WHERE "name" IS NULL;
511
- UPDATE "meyiconnect"."insight_incidents" SET "source" = 'Unknown' WHERE "source" IS NULL;
512
- UPDATE "meyiconnect"."insight_incidents" SET "resource_name" = COALESCE("monitor_name", 'Unknown') WHERE "resource_name" IS NULL;
513
- UPDATE "meyiconnect"."insight_incidents" SET "resource_type" = COALESCE("monitor_type", 'Unknown') WHERE "resource_type" IS NULL;
514
- UPDATE "meyiconnect"."insight_incidents" SET "down_time" = "created_at" WHERE "down_time" IS NULL;
515
-
516
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "name" SET NOT NULL;
517
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "source" SET NOT NULL;
518
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "resource_name" SET NOT NULL;
519
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "resource_type" SET NOT NULL;
520
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "down_time" SET NOT NULL;
521
-
522
- -- Remove NOT NULL from old columns if they exist
523
- IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'monitor_id') THEN
524
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "monitor_id" DROP NOT NULL;
525
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "monitor_name" DROP NOT NULL;
526
- ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "monitor_type" DROP NOT NULL;
527
- END IF;
528
- END $$;
507
+ await db.execute(sql.raw(`
508
+ DO $$
509
+ BEGIN
510
+ UPDATE "meyiconnect"."insight_incidents" SET "name" = COALESCE("monitor_name", 'Unknown') WHERE "name" IS NULL;
511
+ UPDATE "meyiconnect"."insight_incidents" SET "source" = 'Unknown' WHERE "source" IS NULL;
512
+ UPDATE "meyiconnect"."insight_incidents" SET "resource_name" = COALESCE("monitor_name", 'Unknown') WHERE "resource_name" IS NULL;
513
+ UPDATE "meyiconnect"."insight_incidents" SET "resource_type" = COALESCE("monitor_type", 'Unknown') WHERE "resource_type" IS NULL;
514
+ UPDATE "meyiconnect"."insight_incidents" SET "down_time" = "created_at" WHERE "down_time" IS NULL;
515
+
516
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "name" SET NOT NULL;
517
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "source" SET NOT NULL;
518
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "resource_name" SET NOT NULL;
519
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "resource_type" SET NOT NULL;
520
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "down_time" SET NOT NULL;
521
+
522
+ -- Remove NOT NULL from old columns if they exist
523
+ IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'monitor_id') THEN
524
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "monitor_id" DROP NOT NULL;
525
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "monitor_name" DROP NOT NULL;
526
+ ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "monitor_type" DROP NOT NULL;
527
+ END IF;
528
+ END $$;
529
529
  `));
530
530
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'open'`));
531
531
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "analysis_percent" INTEGER NOT NULL DEFAULT 0`));
@@ -539,154 +539,154 @@ export async function runMigrations(db) {
539
539
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "account_id" TEXT`));
540
540
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "account_name" TEXT`));
541
541
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "total_cost" JSONB`));
542
- await db.execute(sql.raw(`
543
- UPDATE "meyiconnect"."insight_incidents" i
544
- SET "account_name" = c."account_name"
545
- FROM "meyiconnect"."connectors" c
546
- WHERE i."account_name" IS NULL
547
- AND i."account_id" IS NOT NULL
548
- AND i."account_id" = c."account_id";
549
- `));
550
- await db.execute(sql.raw(`
551
- CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_comments" (
552
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
553
- "incident_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
554
- "author_id" TEXT,
555
- "author_name" TEXT,
556
- "message" TEXT NOT NULL,
557
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
558
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
559
- )
542
+ await db.execute(sql.raw(`
543
+ UPDATE "meyiconnect"."insight_incidents" i
544
+ SET "account_name" = c."account_name"
545
+ FROM "meyiconnect"."connectors" c
546
+ WHERE i."account_name" IS NULL
547
+ AND i."account_id" IS NOT NULL
548
+ AND i."account_id" = c."account_id";
549
+ `));
550
+ await db.execute(sql.raw(`
551
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_comments" (
552
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
553
+ "incident_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
554
+ "author_id" TEXT,
555
+ "author_name" TEXT,
556
+ "message" TEXT NOT NULL,
557
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
558
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
559
+ )
560
560
  `));
561
561
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS incident_comments_incident_id_idx ON "meyiconnect"."incident_comments" ("incident_id")`));
562
562
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS incident_comments_created_at_idx ON "meyiconnect"."incident_comments" ("created_at")`));
563
563
  // insight_alerts
564
- await db.execute(sql.raw(`
565
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_alerts" (
566
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
567
- "tenant_id" TEXT,
568
- "source_id" UUID,
569
- "source_name" TEXT,
570
- "source_type" TEXT,
571
- "resource_id" TEXT NOT NULL,
572
- "resource_name" TEXT NOT NULL,
573
- "resource_type" TEXT NOT NULL,
574
- "project_tag" TEXT,
575
- "severity" TEXT NOT NULL DEFAULT 'high',
576
- "alert_name" TEXT NOT NULL,
577
- "description" TEXT,
578
- "additional_configuration" JSONB,
579
- "status" TEXT NOT NULL DEFAULT 'open',
580
- "converted_incident_id" UUID,
581
- "incident_id" UUID,
582
- "alias_id" TEXT,
583
- "assignee_id" UUID,
584
- "down_time" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
585
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
586
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
587
- "dup_count" INT NOT NULL DEFAULT 0,
588
- "last_dup_at" TIMESTAMPTZ,
589
- "fingerprint" TEXT,
590
- "dedup_key" TEXT,
591
- "pipeline_status" TEXT DEFAULT 'queued',
592
- "group_id" UUID,
593
- "raw_payload" JSONB
594
- )
595
- `));
596
- await db.execute(sql.raw(`
597
- DO $$
598
- BEGIN
599
- IF NOT EXISTS (
600
- SELECT 1 FROM information_schema.columns
601
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'alert_name'
602
- ) THEN
603
- ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN "alert_name" TEXT;
604
- END IF;
605
-
606
- IF EXISTS (
607
- SELECT 1 FROM information_schema.columns
608
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'title'
609
- ) THEN
610
- ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "title" DROP NOT NULL;
611
- ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "title" SET DEFAULT 'Alert';
612
- UPDATE "meyiconnect"."insight_alerts"
613
- SET "alert_name" = COALESCE("alert_name", "title", 'Alert')
614
- WHERE "alert_name" IS NULL;
615
- ELSE
616
- UPDATE "meyiconnect"."insight_alerts"
617
- SET "alert_name" = COALESCE("alert_name", 'Alert')
618
- WHERE "alert_name" IS NULL;
619
- END IF;
620
-
621
- ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "alert_name" SET NOT NULL;
622
- END $$;
564
+ await db.execute(sql.raw(`
565
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_alerts" (
566
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
567
+ "tenant_id" TEXT,
568
+ "source_id" UUID,
569
+ "source_name" TEXT,
570
+ "source_type" TEXT,
571
+ "resource_id" TEXT NOT NULL,
572
+ "resource_name" TEXT NOT NULL,
573
+ "resource_type" TEXT NOT NULL,
574
+ "project_tag" TEXT,
575
+ "severity" TEXT NOT NULL DEFAULT 'high',
576
+ "alert_name" TEXT NOT NULL,
577
+ "description" TEXT,
578
+ "additional_configuration" JSONB,
579
+ "status" TEXT NOT NULL DEFAULT 'open',
580
+ "converted_incident_id" UUID,
581
+ "incident_id" UUID,
582
+ "alias_id" TEXT,
583
+ "assignee_id" UUID,
584
+ "down_time" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
585
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
586
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
587
+ "dup_count" INT NOT NULL DEFAULT 0,
588
+ "last_dup_at" TIMESTAMPTZ,
589
+ "fingerprint" TEXT,
590
+ "dedup_key" TEXT,
591
+ "pipeline_status" TEXT DEFAULT 'queued',
592
+ "group_id" UUID,
593
+ "raw_payload" JSONB
594
+ )
595
+ `));
596
+ await db.execute(sql.raw(`
597
+ DO $$
598
+ BEGIN
599
+ IF NOT EXISTS (
600
+ SELECT 1 FROM information_schema.columns
601
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'alert_name'
602
+ ) THEN
603
+ ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN "alert_name" TEXT;
604
+ END IF;
605
+
606
+ IF EXISTS (
607
+ SELECT 1 FROM information_schema.columns
608
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'title'
609
+ ) THEN
610
+ ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "title" DROP NOT NULL;
611
+ ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "title" SET DEFAULT 'Alert';
612
+ UPDATE "meyiconnect"."insight_alerts"
613
+ SET "alert_name" = COALESCE("alert_name", "title", 'Alert')
614
+ WHERE "alert_name" IS NULL;
615
+ ELSE
616
+ UPDATE "meyiconnect"."insight_alerts"
617
+ SET "alert_name" = COALESCE("alert_name", 'Alert')
618
+ WHERE "alert_name" IS NULL;
619
+ END IF;
620
+
621
+ ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "alert_name" SET NOT NULL;
622
+ END $$;
623
623
  `));
624
624
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "project_tag" TEXT`));
625
- await db.execute(sql.raw(`
626
- DO $$
627
- BEGIN
628
- IF EXISTS (
629
- SELECT 1 FROM information_schema.columns
630
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'tenant_id' AND data_type = 'uuid'
631
- ) THEN
632
- ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
633
- ELSIF NOT EXISTS (
634
- SELECT 1 FROM information_schema.columns
635
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'tenant_id'
636
- ) THEN
637
- ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN "tenant_id" TEXT;
638
- END IF;
639
- END $$;
625
+ await db.execute(sql.raw(`
626
+ DO $$
627
+ BEGIN
628
+ IF EXISTS (
629
+ SELECT 1 FROM information_schema.columns
630
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'tenant_id' AND data_type = 'uuid'
631
+ ) THEN
632
+ ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "tenant_id" TYPE TEXT USING "tenant_id"::TEXT;
633
+ ELSIF NOT EXISTS (
634
+ SELECT 1 FROM information_schema.columns
635
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'tenant_id'
636
+ ) THEN
637
+ ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN "tenant_id" TEXT;
638
+ END IF;
639
+ END $$;
640
640
  `));
641
641
  await backfill(`UPDATE "meyiconnect"."insight_alerts" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id"::text = ''`);
642
642
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alerts_tenant_id_idx ON "meyiconnect"."insight_alerts" ("tenant_id")`));
643
643
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "alias_id" TEXT`));
644
644
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "additional_configuration" JSONB`));
645
645
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "incident_id" UUID`));
646
- await db.execute(sql.raw(`
647
- DO $$
648
- BEGIN
649
- IF NOT EXISTS (
650
- SELECT 1 FROM information_schema.columns
651
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'down_time'
652
- ) THEN
653
- ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN "down_time" TIMESTAMPTZ;
654
- END IF;
655
-
656
- IF EXISTS (
657
- SELECT 1 FROM information_schema.columns
658
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'alert_time'
659
- ) THEN
660
- UPDATE "meyiconnect"."insight_alerts"
661
- SET "down_time" = COALESCE("down_time", "alert_time", NOW())
662
- WHERE "down_time" IS NULL;
663
- ELSE
664
- UPDATE "meyiconnect"."insight_alerts"
665
- SET "down_time" = COALESCE("down_time", NOW())
666
- WHERE "down_time" IS NULL;
667
- END IF;
668
-
669
- ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "down_time" SET NOT NULL;
670
- ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "down_time" SET DEFAULT NOW();
671
- END $$;
672
- `));
673
- await db.execute(sql.raw(`
674
- DO $$
675
- BEGIN
676
- IF EXISTS (
677
- SELECT 1 FROM information_schema.columns
678
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'promoted_incident_id'
679
- ) THEN
680
- IF NOT EXISTS (
681
- SELECT 1 FROM information_schema.columns
682
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'converted_incident_id'
683
- ) THEN
684
- ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "promoted_incident_id" TO "converted_incident_id";
685
- ELSE
686
- ALTER TABLE "meyiconnect"."insight_alerts" DROP COLUMN "promoted_incident_id";
687
- END IF;
688
- END IF;
689
- END $$;
646
+ await db.execute(sql.raw(`
647
+ DO $$
648
+ BEGIN
649
+ IF NOT EXISTS (
650
+ SELECT 1 FROM information_schema.columns
651
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'down_time'
652
+ ) THEN
653
+ ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN "down_time" TIMESTAMPTZ;
654
+ END IF;
655
+
656
+ IF EXISTS (
657
+ SELECT 1 FROM information_schema.columns
658
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'alert_time'
659
+ ) THEN
660
+ UPDATE "meyiconnect"."insight_alerts"
661
+ SET "down_time" = COALESCE("down_time", "alert_time", NOW())
662
+ WHERE "down_time" IS NULL;
663
+ ELSE
664
+ UPDATE "meyiconnect"."insight_alerts"
665
+ SET "down_time" = COALESCE("down_time", NOW())
666
+ WHERE "down_time" IS NULL;
667
+ END IF;
668
+
669
+ ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "down_time" SET NOT NULL;
670
+ ALTER TABLE "meyiconnect"."insight_alerts" ALTER COLUMN "down_time" SET DEFAULT NOW();
671
+ END $$;
672
+ `));
673
+ await db.execute(sql.raw(`
674
+ DO $$
675
+ BEGIN
676
+ IF EXISTS (
677
+ SELECT 1 FROM information_schema.columns
678
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'promoted_incident_id'
679
+ ) THEN
680
+ IF NOT EXISTS (
681
+ SELECT 1 FROM information_schema.columns
682
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'converted_incident_id'
683
+ ) THEN
684
+ ALTER TABLE "meyiconnect"."insight_alerts" RENAME COLUMN "promoted_incident_id" TO "converted_incident_id";
685
+ ELSE
686
+ ALTER TABLE "meyiconnect"."insight_alerts" DROP COLUMN "promoted_incident_id";
687
+ END IF;
688
+ END IF;
689
+ END $$;
690
690
  `));
691
691
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "converted_incident_id" UUID`));
692
692
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "assignee_id" UUID`));
@@ -694,61 +694,61 @@ export async function runMigrations(db) {
694
694
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "last_dup_at" TIMESTAMPTZ`));
695
695
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "account_id" TEXT`));
696
696
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "account_name" TEXT`));
697
- await db.execute(sql.raw(`
698
- UPDATE "meyiconnect"."insight_alerts" a
699
- SET "account_name" = c."account_name"
700
- FROM "meyiconnect"."connectors" c
701
- WHERE a."account_name" IS NULL
702
- AND a."account_id" IS NOT NULL
703
- AND a."account_id" = c."account_id";
704
- `));
705
- await db.execute(sql.raw(`
706
- UPDATE "meyiconnect"."insight_alerts"
707
- SET "account_id" = COALESCE(
708
- "additional_configuration"->>'accountId',
709
- "raw_payload"->>'account',
710
- "raw_payload"->>'accountId',
711
- "raw_payload"->>'AWSAccountId',
712
- "raw_payload"->'rawEvent'->>'account',
713
- "raw_payload"->'detail'->>'account',
714
- "raw_payload"->'detail'->>'recipientAccountId',
715
- substring("resource_id" FROM 'arn:aws[a-z0-9-]*:cloudwatch:[^:]+:([0-9]{12}):alarm:')
716
- )
717
- WHERE "account_id" IS NULL
718
- AND (
719
- "additional_configuration"->>'accountId' IS NOT NULL
720
- OR "raw_payload"->>'account' IS NOT NULL
721
- OR "raw_payload"->>'accountId' IS NOT NULL
722
- OR "raw_payload"->>'AWSAccountId' IS NOT NULL
723
- OR "raw_payload"->'rawEvent'->>'account' IS NOT NULL
724
- OR "raw_payload"->'detail'->>'account' IS NOT NULL
725
- OR "raw_payload"->'detail'->>'recipientAccountId' IS NOT NULL
726
- OR "resource_id" ~ '^arn:aws[a-z0-9-]*:cloudwatch:[^:]+:[0-9]{12}:alarm:'
727
- )
728
- `));
729
- await db.execute(sql.raw(`
730
- UPDATE "meyiconnect"."insight_alerts"
731
- SET "additional_configuration" = jsonb_set(
732
- COALESCE("additional_configuration", '{}'::jsonb),
733
- '{tags}',
734
- COALESCE("raw_payload"->'tags', "raw_payload"->'Tags', "raw_payload"->'detail'->'tags')
735
- )
736
- WHERE ("additional_configuration" IS NULL OR "additional_configuration"->'tags' IS NULL)
737
- AND (
738
- "raw_payload"->'tags' IS NOT NULL
739
- OR "raw_payload"->'Tags' IS NOT NULL
740
- OR "raw_payload"->'detail'->'tags' IS NOT NULL
741
- )
697
+ await db.execute(sql.raw(`
698
+ UPDATE "meyiconnect"."insight_alerts" a
699
+ SET "account_name" = c."account_name"
700
+ FROM "meyiconnect"."connectors" c
701
+ WHERE a."account_name" IS NULL
702
+ AND a."account_id" IS NOT NULL
703
+ AND a."account_id" = c."account_id";
704
+ `));
705
+ await db.execute(sql.raw(`
706
+ UPDATE "meyiconnect"."insight_alerts"
707
+ SET "account_id" = COALESCE(
708
+ "additional_configuration"->>'accountId',
709
+ "raw_payload"->>'account',
710
+ "raw_payload"->>'accountId',
711
+ "raw_payload"->>'AWSAccountId',
712
+ "raw_payload"->'rawEvent'->>'account',
713
+ "raw_payload"->'detail'->>'account',
714
+ "raw_payload"->'detail'->>'recipientAccountId',
715
+ substring("resource_id" FROM 'arn:aws[a-z0-9-]*:cloudwatch:[^:]+:([0-9]{12}):alarm:')
716
+ )
717
+ WHERE "account_id" IS NULL
718
+ AND (
719
+ "additional_configuration"->>'accountId' IS NOT NULL
720
+ OR "raw_payload"->>'account' IS NOT NULL
721
+ OR "raw_payload"->>'accountId' IS NOT NULL
722
+ OR "raw_payload"->>'AWSAccountId' IS NOT NULL
723
+ OR "raw_payload"->'rawEvent'->>'account' IS NOT NULL
724
+ OR "raw_payload"->'detail'->>'account' IS NOT NULL
725
+ OR "raw_payload"->'detail'->>'recipientAccountId' IS NOT NULL
726
+ OR "resource_id" ~ '^arn:aws[a-z0-9-]*:cloudwatch:[^:]+:[0-9]{12}:alarm:'
727
+ )
728
+ `));
729
+ await db.execute(sql.raw(`
730
+ UPDATE "meyiconnect"."insight_alerts"
731
+ SET "additional_configuration" = jsonb_set(
732
+ COALESCE("additional_configuration", '{}'::jsonb),
733
+ '{tags}',
734
+ COALESCE("raw_payload"->'tags', "raw_payload"->'Tags', "raw_payload"->'detail'->'tags')
735
+ )
736
+ WHERE ("additional_configuration" IS NULL OR "additional_configuration"->'tags' IS NULL)
737
+ AND (
738
+ "raw_payload"->'tags' IS NOT NULL
739
+ OR "raw_payload"->'Tags' IS NOT NULL
740
+ OR "raw_payload"->'detail'->'tags' IS NOT NULL
741
+ )
742
742
  `));
743
743
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alerts_status_idx ON "meyiconnect"."insight_alerts" ("status")`));
744
744
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alerts_source_idx ON "meyiconnect"."insight_alerts" ("source_id")`));
745
745
  // insight_settings
746
- await db.execute(sql.raw(`
747
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_settings" (
748
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
749
-
750
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
751
- )
746
+ await db.execute(sql.raw(`
747
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_settings" (
748
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
749
+
750
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
751
+ )
752
752
  `));
753
753
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_settings" ADD COLUMN IF NOT EXISTS "aws_bedrock_access_key_id" TEXT`));
754
754
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_settings" ADD COLUMN IF NOT EXISTS "aws_bedrock_secret_access_key" TEXT`));
@@ -757,19 +757,19 @@ export async function runMigrations(db) {
757
757
  await backfill(`UPDATE "meyiconnect"."insight_settings" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id" = ''`);
758
758
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_settings_tenant_id_idx ON "meyiconnect"."insight_settings" ("tenant_id")`));
759
759
  // notification_providers
760
- await db.execute(sql.raw(`
761
- CREATE TABLE IF NOT EXISTS "meyiconnect"."notification_providers" (
762
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
763
- "tenant_id" TEXT,
764
- "provider_name" VARCHAR(50) NOT NULL,
765
- "display_name" VARCHAR(100) NOT NULL,
766
- "is_enabled" BOOLEAN NOT NULL DEFAULT false,
767
- "config" JSONB,
768
- "description" TEXT,
769
- "icon" TEXT,
770
- "created_at" TIMESTAMP DEFAULT NOW(),
771
- "updated_at" TIMESTAMP DEFAULT NOW()
772
- )
760
+ await db.execute(sql.raw(`
761
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."notification_providers" (
762
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
763
+ "tenant_id" TEXT,
764
+ "provider_name" VARCHAR(50) NOT NULL,
765
+ "display_name" VARCHAR(100) NOT NULL,
766
+ "is_enabled" BOOLEAN NOT NULL DEFAULT false,
767
+ "config" JSONB,
768
+ "description" TEXT,
769
+ "icon" TEXT,
770
+ "created_at" TIMESTAMP DEFAULT NOW(),
771
+ "updated_at" TIMESTAMP DEFAULT NOW()
772
+ )
773
773
  `));
774
774
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."notification_providers" ADD COLUMN IF NOT EXISTS "tenant_id" TEXT`)).catch(() => { });
775
775
  await backfill(`UPDATE "meyiconnect"."notification_providers" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id"::text = ''`).catch(() => { });
@@ -778,50 +778,50 @@ export async function runMigrations(db) {
778
778
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS notification_providers_tenant_name_unique ON "meyiconnect"."notification_providers" ("tenant_id", "provider_name")`)).catch(() => { });
779
779
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS notification_providers_tenant_id_idx ON "meyiconnect"."notification_providers" ("tenant_id")`)).catch(() => { });
780
780
  // incident_events — timestamped audit/timeline log (status changes, assigns, analysis, comments)
781
- await db.execute(sql.raw(`
782
- CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_events" (
783
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
784
- "incident_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
785
- "type" TEXT NOT NULL,
786
- "actor" TEXT,
787
- "message" TEXT,
788
- "before_value" TEXT,
789
- "after_value" TEXT,
790
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
791
- )
781
+ await db.execute(sql.raw(`
782
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_events" (
783
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
784
+ "incident_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
785
+ "type" TEXT NOT NULL,
786
+ "actor" TEXT,
787
+ "message" TEXT,
788
+ "before_value" TEXT,
789
+ "after_value" TEXT,
790
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
791
+ )
792
792
  `));
793
793
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS incident_events_incident_id_idx ON "meyiconnect"."incident_events" ("incident_id")`));
794
794
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS incident_events_created_at_idx ON "meyiconnect"."incident_events" ("created_at")`));
795
795
  // incident_logs
796
- await db.execute(sql.raw(`
797
- CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_logs" (
798
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
799
- "incident_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
800
- "ec2_details" JSONB,
801
- "ec2_status_checks" JSONB,
802
- "cloudwatch_metrics" JSONB,
803
- "raw_logs" JSONB,
804
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
805
- )
796
+ await db.execute(sql.raw(`
797
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_logs" (
798
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
799
+ "incident_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
800
+ "ec2_details" JSONB,
801
+ "ec2_status_checks" JSONB,
802
+ "cloudwatch_metrics" JSONB,
803
+ "raw_logs" JSONB,
804
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
805
+ )
806
806
  `));
807
807
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS incident_logs_incident_id_idx ON "meyiconnect"."incident_logs" ("incident_id")`));
808
808
  // insight_sops
809
- await db.execute(sql.raw(`
810
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_sops" (
811
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
812
- "sop_id" TEXT NOT NULL,
813
- "title" TEXT NOT NULL,
814
- "alert_type" TEXT,
815
- "service" TEXT,
816
- "severity" TEXT,
817
- "status" TEXT NOT NULL DEFAULT 'draft',
818
- "description" TEXT,
819
- "steps" TEXT,
820
- "error_response" TEXT,
821
- "created_by" TEXT DEFAULT 'Manual',
822
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
823
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
824
- )
809
+ await db.execute(sql.raw(`
810
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_sops" (
811
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
812
+ "sop_id" TEXT NOT NULL,
813
+ "title" TEXT NOT NULL,
814
+ "alert_type" TEXT,
815
+ "service" TEXT,
816
+ "severity" TEXT,
817
+ "status" TEXT NOT NULL DEFAULT 'draft',
818
+ "description" TEXT,
819
+ "steps" TEXT,
820
+ "error_response" TEXT,
821
+ "created_by" TEXT DEFAULT 'Manual',
822
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
823
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
824
+ )
825
825
  `));
826
826
  await db.execute(sql.raw(`DROP INDEX IF EXISTS "meyiconnect"."insight_sops_sop_id_unique"`)).catch(() => { });
827
827
  await db.execute(sql.raw(`DROP INDEX IF EXISTS insight_sops_sop_id_unique`)).catch(() => { });
@@ -831,46 +831,46 @@ export async function runMigrations(db) {
831
831
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sops" ADD COLUMN IF NOT EXISTS "error_response" TEXT`));
832
832
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sops" ADD COLUMN IF NOT EXISTS "source_id" UUID`));
833
833
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sops" ADD COLUMN IF NOT EXISTS "project_id" UUID`));
834
- await db.execute(sql.raw(`
835
- DO $$
836
- BEGIN
837
- IF EXISTS (
838
- SELECT 1 FROM information_schema.columns
839
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sops' AND column_name = 'incident_id'
840
- ) THEN
841
- IF NOT EXISTS (
842
- SELECT 1 FROM information_schema.columns
843
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sops' AND column_name = 'alert_id'
844
- ) THEN
845
- -- Drop old FK before renaming to ensure it points to the correct table
846
- ALTER TABLE "meyiconnect"."insight_sops" DROP CONSTRAINT IF EXISTS insight_sops_incident_id_fkey;
847
- ALTER TABLE "meyiconnect"."insight_sops" RENAME COLUMN "incident_id" TO "alert_id";
848
- ELSE
849
- ALTER TABLE "meyiconnect"."insight_sops" DROP COLUMN "incident_id";
850
- END IF;
851
- END IF;
852
- END $$;
834
+ await db.execute(sql.raw(`
835
+ DO $$
836
+ BEGIN
837
+ IF EXISTS (
838
+ SELECT 1 FROM information_schema.columns
839
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sops' AND column_name = 'incident_id'
840
+ ) THEN
841
+ IF NOT EXISTS (
842
+ SELECT 1 FROM information_schema.columns
843
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_sops' AND column_name = 'alert_id'
844
+ ) THEN
845
+ -- Drop old FK before renaming to ensure it points to the correct table
846
+ ALTER TABLE "meyiconnect"."insight_sops" DROP CONSTRAINT IF EXISTS insight_sops_incident_id_fkey;
847
+ ALTER TABLE "meyiconnect"."insight_sops" RENAME COLUMN "incident_id" TO "alert_id";
848
+ ELSE
849
+ ALTER TABLE "meyiconnect"."insight_sops" DROP COLUMN "incident_id";
850
+ END IF;
851
+ END IF;
852
+ END $$;
853
853
  `));
854
854
  // Drop constraint anyway in case we renamed it but left the constraint intact previously
855
855
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sops" DROP CONSTRAINT IF EXISTS insight_sops_incident_id_fkey;`));
856
856
  // Add column if it doesn't exist
857
857
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sops" ADD COLUMN IF NOT EXISTS "alert_id" UUID`));
858
858
  // Explicitly add the new foreign key constraint if it doesn't exist
859
- await db.execute(sql.raw(`
860
- DO $$
861
- BEGIN
862
- IF NOT EXISTS (
863
- SELECT 1 FROM information_schema.table_constraints
864
- WHERE constraint_schema = 'meyiconnect' AND constraint_name = 'insight_sops_alert_id_fkey'
865
- ) THEN
866
- -- Clear invalid alert_ids (which are actually old incident_ids) before adding the constraint
867
- UPDATE "meyiconnect"."insight_sops"
868
- SET alert_id = NULL
869
- WHERE alert_id IS NOT NULL AND alert_id NOT IN (SELECT id FROM "meyiconnect"."insight_alerts");
870
-
871
- ALTER TABLE "meyiconnect"."insight_sops" ADD CONSTRAINT insight_sops_alert_id_fkey FOREIGN KEY ("alert_id") REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE SET NULL;
872
- END IF;
873
- END $$;
859
+ await db.execute(sql.raw(`
860
+ DO $$
861
+ BEGIN
862
+ IF NOT EXISTS (
863
+ SELECT 1 FROM information_schema.table_constraints
864
+ WHERE constraint_schema = 'meyiconnect' AND constraint_name = 'insight_sops_alert_id_fkey'
865
+ ) THEN
866
+ -- Clear invalid alert_ids (which are actually old incident_ids) before adding the constraint
867
+ UPDATE "meyiconnect"."insight_sops"
868
+ SET alert_id = NULL
869
+ WHERE alert_id IS NOT NULL AND alert_id NOT IN (SELECT id FROM "meyiconnect"."insight_alerts");
870
+
871
+ ALTER TABLE "meyiconnect"."insight_sops" ADD CONSTRAINT insight_sops_alert_id_fkey FOREIGN KEY ("alert_id") REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE SET NULL;
872
+ END IF;
873
+ END $$;
874
874
  `));
875
875
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_sops" ADD COLUMN IF NOT EXISTS "alert_name" TEXT`));
876
876
  await db.execute(sql.raw(`DROP INDEX IF EXISTS "meyiconnect"."insight_sops_alert_name_unique"`)).catch(() => { });
@@ -883,74 +883,74 @@ export async function runMigrations(db) {
883
883
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_sops_tenant_id_idx ON "meyiconnect"."insight_sops" ("tenant_id")`));
884
884
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS insight_sops_tenant_sop_id_unique ON "meyiconnect"."insight_sops" ("tenant_id", "sop_id")`));
885
885
  // incident_evidence
886
- await db.execute(sql.raw(`
887
- CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_evidence" (
888
- "incident_id" UUID PRIMARY KEY REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
889
- "evidence_text" JSONB NOT NULL DEFAULT '[]'::jsonb,
890
- "investigation_findings" JSONB NOT NULL DEFAULT '[]'::jsonb,
891
- "rca_signals" JSONB NOT NULL DEFAULT '{}'::jsonb,
892
- "tool_calls_made" JSONB NOT NULL DEFAULT '[]'::jsonb,
893
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
894
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
895
- )
886
+ await db.execute(sql.raw(`
887
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."incident_evidence" (
888
+ "incident_id" UUID PRIMARY KEY REFERENCES "meyiconnect"."insight_incidents"("id") ON DELETE CASCADE,
889
+ "evidence_text" JSONB NOT NULL DEFAULT '[]'::jsonb,
890
+ "investigation_findings" JSONB NOT NULL DEFAULT '[]'::jsonb,
891
+ "rca_signals" JSONB NOT NULL DEFAULT '{}'::jsonb,
892
+ "tool_calls_made" JSONB NOT NULL DEFAULT '[]'::jsonb,
893
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
894
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
895
+ )
896
896
  `));
897
897
  // insight_workflows
898
- await db.execute(sql.raw(`
899
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_workflows" (
900
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
901
- "name" TEXT NOT NULL,
902
- "description" TEXT,
903
- "is_active" BOOLEAN NOT NULL DEFAULT true,
904
- "priority" INTEGER NOT NULL DEFAULT 0,
905
- "conditions" JSONB NOT NULL DEFAULT '[]'::jsonb,
906
- "actions" JSONB NOT NULL DEFAULT '[]'::jsonb,
907
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
908
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
909
- )
910
- `));
911
- await db.execute(sql.raw(`
912
- ALTER TABLE "meyiconnect"."insight_workflows"
913
- ADD COLUMN IF NOT EXISTS "yaml_definition" TEXT NOT NULL DEFAULT ''
898
+ await db.execute(sql.raw(`
899
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_workflows" (
900
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
901
+ "name" TEXT NOT NULL,
902
+ "description" TEXT,
903
+ "is_active" BOOLEAN NOT NULL DEFAULT true,
904
+ "priority" INTEGER NOT NULL DEFAULT 0,
905
+ "conditions" JSONB NOT NULL DEFAULT '[]'::jsonb,
906
+ "actions" JSONB NOT NULL DEFAULT '[]'::jsonb,
907
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
908
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
909
+ )
910
+ `));
911
+ await db.execute(sql.raw(`
912
+ ALTER TABLE "meyiconnect"."insight_workflows"
913
+ ADD COLUMN IF NOT EXISTS "yaml_definition" TEXT NOT NULL DEFAULT ''
914
914
  `));
915
915
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_workflows" ADD COLUMN IF NOT EXISTS "tenant_id" TEXT`));
916
916
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_workflows" ADD COLUMN IF NOT EXISTS "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE`));
917
917
  await backfill(`UPDATE "meyiconnect"."insight_workflows" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id" = ''`);
918
918
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_workflows_tenant_id_idx ON "meyiconnect"."insight_workflows" ("tenant_id")`));
919
- await db.execute(sql.raw(`
920
- ALTER TABLE "meyiconnect"."insight_workflows"
921
- DROP COLUMN IF EXISTS "conditions"
919
+ await db.execute(sql.raw(`
920
+ ALTER TABLE "meyiconnect"."insight_workflows"
921
+ DROP COLUMN IF EXISTS "conditions"
922
922
  `));
923
- await db.execute(sql.raw(`
924
- ALTER TABLE "meyiconnect"."insight_workflows"
925
- DROP COLUMN IF EXISTS "actions"
923
+ await db.execute(sql.raw(`
924
+ ALTER TABLE "meyiconnect"."insight_workflows"
925
+ DROP COLUMN IF EXISTS "actions"
926
926
  `));
927
- await db.execute(sql.raw(`
928
- ALTER TABLE "meyiconnect"."insight_workflows"
929
- DROP COLUMN IF EXISTS "description"
927
+ await db.execute(sql.raw(`
928
+ ALTER TABLE "meyiconnect"."insight_workflows"
929
+ DROP COLUMN IF EXISTS "description"
930
930
  `));
931
931
  // insight_workflow_executions
932
- await db.execute(sql.raw(`
933
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_workflow_executions" (
934
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
935
- "workflow_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_workflows"("id") ON DELETE CASCADE,
936
- "alert_id" UUID REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE SET NULL,
937
- "status" TEXT NOT NULL DEFAULT 'pending',
938
- "actions_executed" JSONB NOT NULL DEFAULT '[]'::jsonb,
939
- "error_message" TEXT,
940
- "triggered_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
941
- "completed_at" TIMESTAMPTZ
942
- )
932
+ await db.execute(sql.raw(`
933
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_workflow_executions" (
934
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
935
+ "workflow_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_workflows"("id") ON DELETE CASCADE,
936
+ "alert_id" UUID REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE SET NULL,
937
+ "status" TEXT NOT NULL DEFAULT 'pending',
938
+ "actions_executed" JSONB NOT NULL DEFAULT '[]'::jsonb,
939
+ "error_message" TEXT,
940
+ "triggered_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
941
+ "completed_at" TIMESTAMPTZ
942
+ )
943
943
  `));
944
944
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_workflow_executions_workflow_id_idx ON "meyiconnect"."insight_workflow_executions" ("workflow_id")`));
945
945
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_workflow_executions_alert_id_idx ON "meyiconnect"."insight_workflow_executions" ("alert_id")`));
946
946
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_workflow_executions_status_idx ON "meyiconnect"."insight_workflow_executions" ("status")`));
947
947
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_workflow_executions" ADD COLUMN IF NOT EXISTS "tenant_id" TEXT`));
948
- await db.execute(sql.raw(`
949
- UPDATE "meyiconnect"."insight_workflow_executions" e
950
- SET "tenant_id" = ${defaultTenantId ? `COALESCE(w."tenant_id", '${defaultTenantSql}')` : `w."tenant_id"`}
951
- FROM "meyiconnect"."insight_workflows" w
952
- WHERE e."workflow_id" = w."id"
953
- AND (e."tenant_id" IS NULL OR e."tenant_id" = '')
948
+ await db.execute(sql.raw(`
949
+ UPDATE "meyiconnect"."insight_workflow_executions" e
950
+ SET "tenant_id" = ${defaultTenantId ? `COALESCE(w."tenant_id", '${defaultTenantSql}')` : `w."tenant_id"`}
951
+ FROM "meyiconnect"."insight_workflows" w
952
+ WHERE e."workflow_id" = w."id"
953
+ AND (e."tenant_id" IS NULL OR e."tenant_id" = '')
954
954
  `));
955
955
  await backfill(`UPDATE "meyiconnect"."insight_workflow_executions" SET "tenant_id" = '${defaultTenantSql}' WHERE "tenant_id" IS NULL OR "tenant_id" = ''`);
956
956
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_workflow_executions_tenant_id_idx ON "meyiconnect"."insight_workflow_executions" ("tenant_id")`));
@@ -962,40 +962,40 @@ export async function runMigrations(db) {
962
962
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alerts" ADD COLUMN IF NOT EXISTS "raw_payload" JSONB`));
963
963
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alerts_pipeline_status_idx ON "meyiconnect"."insight_alerts" ("pipeline_status")`));
964
964
  // ─── insight_provider_configs ────────────────────────────────────────────────
965
- await db.execute(sql.raw(`
966
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_provider_configs" (
967
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
968
- "tenant_id" TEXT,
969
- "llm_provider_name" TEXT NOT NULL,
970
- "llm_model_name" TEXT,
971
- "llm_model_id" TEXT,
972
- "llm_api_key" TEXT,
973
- "is_active" BOOLEAN NOT NULL DEFAULT true,
974
- "auth_type" TEXT,
975
- "aws_region" TEXT,
976
- "aws_access_key_id" TEXT,
977
- "aws_secret_access_key" TEXT,
978
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
979
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
980
- )
965
+ await db.execute(sql.raw(`
966
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_provider_configs" (
967
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
968
+ "tenant_id" TEXT,
969
+ "llm_provider_name" TEXT NOT NULL,
970
+ "llm_model_name" TEXT,
971
+ "llm_model_id" TEXT,
972
+ "llm_api_key" TEXT,
973
+ "is_active" BOOLEAN NOT NULL DEFAULT true,
974
+ "auth_type" TEXT,
975
+ "aws_region" TEXT,
976
+ "aws_access_key_id" TEXT,
977
+ "aws_secret_access_key" TEXT,
978
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
979
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
980
+ )
981
981
  `));
982
982
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_provider_configs_tenant_id_idx ON "meyiconnect"."insight_provider_configs" ("tenant_id")`));
983
983
  // ─── insight_normalization_configs ───────────────────────────────────────────
984
- await db.execute(sql.raw(`
985
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_normalization_configs" (
986
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
987
- "tenant_id" TEXT,
988
- "name" TEXT NOT NULL,
989
- "priority" INTEGER NOT NULL DEFAULT 0,
990
- "is_active" BOOLEAN NOT NULL DEFAULT true,
991
- "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
992
- "source_type" TEXT,
993
- "field_mapping" TEXT NOT NULL DEFAULT '',
994
- "yaml_definition" TEXT NOT NULL DEFAULT '',
995
- "is_system" BOOLEAN NOT NULL DEFAULT false,
996
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
997
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
998
- )
984
+ await db.execute(sql.raw(`
985
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_normalization_configs" (
986
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
987
+ "tenant_id" TEXT,
988
+ "name" TEXT NOT NULL,
989
+ "priority" INTEGER NOT NULL DEFAULT 0,
990
+ "is_active" BOOLEAN NOT NULL DEFAULT true,
991
+ "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
992
+ "source_type" TEXT,
993
+ "field_mapping" TEXT NOT NULL DEFAULT '',
994
+ "yaml_definition" TEXT NOT NULL DEFAULT '',
995
+ "is_system" BOOLEAN NOT NULL DEFAULT false,
996
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
997
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
998
+ )
999
999
  `));
1000
1000
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_normalization_configs" ADD COLUMN IF NOT EXISTS "is_system" BOOLEAN NOT NULL DEFAULT false`));
1001
1001
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_normalization_configs_priority_idx ON "meyiconnect"."insight_normalization_configs" ("priority")`));
@@ -1004,25 +1004,25 @@ export async function runMigrations(db) {
1004
1004
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_normalization_configs" ADD COLUMN IF NOT EXISTS "tenant_id" TEXT`));
1005
1005
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_normalization_configs_tenant_id_idx ON "meyiconnect"."insight_normalization_configs" ("tenant_id")`));
1006
1006
  // ─── insight_dedup_configs ───────────────────────────────────────────────────
1007
- await db.execute(sql.raw(`
1008
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_dedup_configs" (
1009
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1010
- "tenant_id" TEXT,
1011
- "name" TEXT NOT NULL,
1012
- "priority" INTEGER NOT NULL DEFAULT 0,
1013
- "is_active" BOOLEAN NOT NULL DEFAULT true,
1014
- "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
1015
- "source_type" TEXT,
1016
- "window_seconds" INTEGER NOT NULL DEFAULT 300,
1017
- "key_fields" JSONB NOT NULL DEFAULT '[]'::jsonb,
1018
- "flap_threshold" INTEGER NOT NULL DEFAULT 4,
1019
- "flap_window_seconds" INTEGER NOT NULL DEFAULT 600,
1020
- "on_flap" TEXT NOT NULL DEFAULT 'none',
1021
- "yaml_definition" TEXT NOT NULL DEFAULT '',
1022
- "is_system" BOOLEAN NOT NULL DEFAULT false,
1023
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1024
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1025
- )
1007
+ await db.execute(sql.raw(`
1008
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_dedup_configs" (
1009
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1010
+ "tenant_id" TEXT,
1011
+ "name" TEXT NOT NULL,
1012
+ "priority" INTEGER NOT NULL DEFAULT 0,
1013
+ "is_active" BOOLEAN NOT NULL DEFAULT true,
1014
+ "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
1015
+ "source_type" TEXT,
1016
+ "window_seconds" INTEGER NOT NULL DEFAULT 300,
1017
+ "key_fields" JSONB NOT NULL DEFAULT '[]'::jsonb,
1018
+ "flap_threshold" INTEGER NOT NULL DEFAULT 4,
1019
+ "flap_window_seconds" INTEGER NOT NULL DEFAULT 600,
1020
+ "on_flap" TEXT NOT NULL DEFAULT 'none',
1021
+ "yaml_definition" TEXT NOT NULL DEFAULT '',
1022
+ "is_system" BOOLEAN NOT NULL DEFAULT false,
1023
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1024
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1025
+ )
1026
1026
  `));
1027
1027
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_dedup_configs" ADD COLUMN IF NOT EXISTS "is_system" BOOLEAN NOT NULL DEFAULT false`));
1028
1028
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_dedup_configs_priority_idx ON "meyiconnect"."insight_dedup_configs" ("priority")`));
@@ -1031,25 +1031,25 @@ export async function runMigrations(db) {
1031
1031
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_dedup_configs" ADD COLUMN IF NOT EXISTS "tenant_id" TEXT`));
1032
1032
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_dedup_configs_tenant_id_idx ON "meyiconnect"."insight_dedup_configs" ("tenant_id")`));
1033
1033
  // ─── insight_grouping_configs ────────────────────────────────────────────────
1034
- await db.execute(sql.raw(`
1035
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_grouping_configs" (
1036
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1037
- "tenant_id" TEXT,
1038
- "name" TEXT NOT NULL,
1039
- "priority" INTEGER NOT NULL DEFAULT 0,
1040
- "is_active" BOOLEAN NOT NULL DEFAULT true,
1041
- "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
1042
- "source_type" TEXT,
1043
- "group_by" JSONB NOT NULL DEFAULT '[]'::jsonb,
1044
- "timeout_seconds" INTEGER NOT NULL DEFAULT 600,
1045
- "max_alarms" INTEGER NOT NULL DEFAULT 50,
1046
- "incident_title_template" TEXT DEFAULT '{{count}} alerts on {{resourceName}}',
1047
- "use_ai_generate" BOOLEAN NOT NULL DEFAULT false,
1048
- "yaml_definition" TEXT NOT NULL DEFAULT '',
1049
- "is_system" BOOLEAN NOT NULL DEFAULT false,
1050
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1051
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1052
- )
1034
+ await db.execute(sql.raw(`
1035
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_grouping_configs" (
1036
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1037
+ "tenant_id" TEXT,
1038
+ "name" TEXT NOT NULL,
1039
+ "priority" INTEGER NOT NULL DEFAULT 0,
1040
+ "is_active" BOOLEAN NOT NULL DEFAULT true,
1041
+ "source_id" UUID REFERENCES "meyiconnect"."insight_sources"("id") ON DELETE CASCADE,
1042
+ "source_type" TEXT,
1043
+ "group_by" JSONB NOT NULL DEFAULT '[]'::jsonb,
1044
+ "timeout_seconds" INTEGER NOT NULL DEFAULT 600,
1045
+ "max_alarms" INTEGER NOT NULL DEFAULT 50,
1046
+ "incident_title_template" TEXT DEFAULT '{{count}} alerts on {{resourceName}}',
1047
+ "use_ai_generate" BOOLEAN NOT NULL DEFAULT false,
1048
+ "yaml_definition" TEXT NOT NULL DEFAULT '',
1049
+ "is_system" BOOLEAN NOT NULL DEFAULT false,
1050
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1051
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1052
+ )
1053
1053
  `));
1054
1054
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_grouping_configs" ADD COLUMN IF NOT EXISTS "is_system" BOOLEAN NOT NULL DEFAULT false`));
1055
1055
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_grouping_configs_priority_idx ON "meyiconnect"."insight_grouping_configs" ("priority")`));
@@ -1060,48 +1060,48 @@ export async function runMigrations(db) {
1060
1060
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_grouping_configs" ADD COLUMN IF NOT EXISTS "use_ai_generate" BOOLEAN NOT NULL DEFAULT false`));
1061
1061
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_grouping_configs" ADD COLUMN IF NOT EXISTS "topology_tag_mapping" TEXT`));
1062
1062
  // ─── insight_alarm_groups ────────────────────────────────────────────────────
1063
- await db.execute(sql.raw(`
1064
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_alarm_groups" (
1065
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1066
- "tenant_id" TEXT,
1067
- "group_key" TEXT NOT NULL,
1068
- "config_id" UUID REFERENCES "meyiconnect"."insight_grouping_configs"("id") ON DELETE SET NULL,
1069
- "status" TEXT NOT NULL DEFAULT 'buffering',
1070
- "first_alarm_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1071
- "last_alarm_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1072
- "incident_id" UUID,
1073
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1074
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1075
- )
1063
+ await db.execute(sql.raw(`
1064
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_alarm_groups" (
1065
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1066
+ "tenant_id" TEXT,
1067
+ "group_key" TEXT NOT NULL,
1068
+ "config_id" UUID REFERENCES "meyiconnect"."insight_grouping_configs"("id") ON DELETE SET NULL,
1069
+ "status" TEXT NOT NULL DEFAULT 'buffering',
1070
+ "first_alarm_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1071
+ "last_alarm_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1072
+ "incident_id" UUID,
1073
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1074
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1075
+ )
1076
1076
  `));
1077
1077
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alarm_groups_flush_idx ON "meyiconnect"."insight_alarm_groups" ("status", "last_alarm_at")`));
1078
1078
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alarm_groups_group_key_idx ON "meyiconnect"."insight_alarm_groups" ("group_key")`));
1079
1079
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_alarm_groups" ADD COLUMN IF NOT EXISTS "tenant_id" TEXT`));
1080
1080
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alarm_groups_tenant_id_idx ON "meyiconnect"."insight_alarm_groups" ("tenant_id")`));
1081
1081
  // ─── insight_alarm_group_members (P1 junction table) ─────────────────────────
1082
- await db.execute(sql.raw(`
1083
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_alarm_group_members" (
1084
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1085
- "group_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_alarm_groups"("id") ON DELETE CASCADE,
1086
- "alarm_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE CASCADE,
1087
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1088
- )
1082
+ await db.execute(sql.raw(`
1083
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_alarm_group_members" (
1084
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1085
+ "group_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_alarm_groups"("id") ON DELETE CASCADE,
1086
+ "alarm_id" UUID NOT NULL REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE CASCADE,
1087
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1088
+ )
1089
1089
  `));
1090
1090
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alarm_group_members_group_id_idx ON "meyiconnect"."insight_alarm_group_members" ("group_id")`));
1091
1091
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_alarm_group_members_alarm_id_idx ON "meyiconnect"."insight_alarm_group_members" ("alarm_id")`));
1092
1092
  await db.execute(sql.raw(`CREATE UNIQUE INDEX IF NOT EXISTS insight_alarm_group_members_unique_idx ON "meyiconnect"."insight_alarm_group_members" ("group_id", "alarm_id")`));
1093
1093
  // ─── insight_pipeline_events ─────────────────────────────────────────────────
1094
- await db.execute(sql.raw(`
1095
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_pipeline_events" (
1096
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1097
- "tenant_id" TEXT,
1098
- "alarm_id" UUID REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE CASCADE,
1099
- "stage" TEXT NOT NULL,
1100
- "status" TEXT NOT NULL,
1101
- "matched_config_id" UUID,
1102
- "meta" JSONB,
1103
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1104
- )
1094
+ await db.execute(sql.raw(`
1095
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_pipeline_events" (
1096
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1097
+ "tenant_id" TEXT,
1098
+ "alarm_id" UUID REFERENCES "meyiconnect"."insight_alerts"("id") ON DELETE CASCADE,
1099
+ "stage" TEXT NOT NULL,
1100
+ "status" TEXT NOT NULL,
1101
+ "matched_config_id" UUID,
1102
+ "meta" JSONB,
1103
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1104
+ )
1105
1105
  `));
1106
1106
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_pipeline_events_alarm_id_idx ON "meyiconnect"."insight_pipeline_events" ("alarm_id")`));
1107
1107
  await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS insight_pipeline_events_stage_idx ON "meyiconnect"."insight_pipeline_events" ("stage")`));
@@ -1117,46 +1117,46 @@ export async function runMigrations(db) {
1117
1117
  if (configs.normalization) {
1118
1118
  const parsed = yaml.load(configs.normalization.yamlDefinition);
1119
1119
  const name = parsed.name || `${sourceType} Default Normalization`;
1120
- await db.execute(sql.raw(`
1121
- UPDATE "meyiconnect"."insight_normalization_configs"
1122
- SET "field_mapping" = $token$${configs.normalization.fieldMapping}$token$,
1123
- "yaml_definition" = $token$${configs.normalization.yamlDefinition}$token$
1124
- WHERE "source_type" = '${sourceType}' AND "is_system" = true
1120
+ await db.execute(sql.raw(`
1121
+ UPDATE "meyiconnect"."insight_normalization_configs"
1122
+ SET "field_mapping" = $token$${configs.normalization.fieldMapping}$token$,
1123
+ "yaml_definition" = $token$${configs.normalization.yamlDefinition}$token$
1124
+ WHERE "source_type" = '${sourceType}' AND "is_system" = true
1125
1125
  `));
1126
- await db.execute(sql.raw(`
1127
- INSERT INTO "meyiconnect"."insight_normalization_configs"
1128
- ("name", "priority", "is_active", "source_type", "field_mapping", "yaml_definition", "is_system")
1129
- SELECT '${name}', 10, true, '${sourceType}', $token$${configs.normalization.fieldMapping}$token$, $token$${configs.normalization.yamlDefinition}$token$, true
1130
- WHERE NOT EXISTS (
1131
- SELECT 1 FROM "meyiconnect"."insight_normalization_configs"
1132
- WHERE "source_type" = '${sourceType}' AND "is_system" = true
1133
- )
1126
+ await db.execute(sql.raw(`
1127
+ INSERT INTO "meyiconnect"."insight_normalization_configs"
1128
+ ("name", "priority", "is_active", "source_type", "field_mapping", "yaml_definition", "is_system")
1129
+ SELECT '${name}', 10, true, '${sourceType}', $token$${configs.normalization.fieldMapping}$token$, $token$${configs.normalization.yamlDefinition}$token$, true
1130
+ WHERE NOT EXISTS (
1131
+ SELECT 1 FROM "meyiconnect"."insight_normalization_configs"
1132
+ WHERE "source_type" = '${sourceType}' AND "is_system" = true
1133
+ )
1134
1134
  `));
1135
1135
  }
1136
1136
  if (configs.dedup) {
1137
1137
  const parsed = yaml.load(configs.dedup.yamlDefinition);
1138
1138
  const name = parsed.name || `${sourceType} Default Deduplication`;
1139
- await db.execute(sql.raw(`
1140
- INSERT INTO "meyiconnect"."insight_dedup_configs"
1141
- ("name", "priority", "is_active", "source_type", "yaml_definition", "is_system")
1142
- SELECT '${name}', 10, true, '${sourceType}', $token$${configs.dedup.yamlDefinition}$token$, true
1143
- WHERE NOT EXISTS (
1144
- SELECT 1 FROM "meyiconnect"."insight_dedup_configs"
1145
- WHERE "source_type" = '${sourceType}' AND "is_system" = true
1146
- )
1139
+ await db.execute(sql.raw(`
1140
+ INSERT INTO "meyiconnect"."insight_dedup_configs"
1141
+ ("name", "priority", "is_active", "source_type", "yaml_definition", "is_system")
1142
+ SELECT '${name}', 10, true, '${sourceType}', $token$${configs.dedup.yamlDefinition}$token$, true
1143
+ WHERE NOT EXISTS (
1144
+ SELECT 1 FROM "meyiconnect"."insight_dedup_configs"
1145
+ WHERE "source_type" = '${sourceType}' AND "is_system" = true
1146
+ )
1147
1147
  `));
1148
1148
  }
1149
1149
  if (configs.grouping) {
1150
1150
  const parsed = yaml.load(configs.grouping.yamlDefinition);
1151
1151
  const name = parsed.name || `${sourceType} Default Grouping`;
1152
- await db.execute(sql.raw(`
1153
- INSERT INTO "meyiconnect"."insight_grouping_configs"
1154
- ("name", "priority", "is_active", "source_type", "yaml_definition", "is_system")
1155
- SELECT '${name}', 10, true, '${sourceType}', $token$${configs.grouping.yamlDefinition}$token$, true
1156
- WHERE NOT EXISTS (
1157
- SELECT 1 FROM "meyiconnect"."insight_grouping_configs"
1158
- WHERE "source_type" = '${sourceType}' AND "is_system" = true
1159
- )
1152
+ await db.execute(sql.raw(`
1153
+ INSERT INTO "meyiconnect"."insight_grouping_configs"
1154
+ ("name", "priority", "is_active", "source_type", "yaml_definition", "is_system")
1155
+ SELECT '${name}', 10, true, '${sourceType}', $token$${configs.grouping.yamlDefinition}$token$, true
1156
+ WHERE NOT EXISTS (
1157
+ SELECT 1 FROM "meyiconnect"."insight_grouping_configs"
1158
+ WHERE "source_type" = '${sourceType}' AND "is_system" = true
1159
+ )
1160
1160
  `));
1161
1161
  }
1162
1162
  }
@@ -1167,371 +1167,371 @@ export async function runMigrations(db) {
1167
1167
  // ─── insight_connectors ────────────────────────────────────────────────────
1168
1168
  // Denormalized connector table written by marketplace-service during
1169
1169
  // onboarding. insight-server reads from this table for the Connectors page.
1170
- await db.execute(sql.raw(`
1171
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_connectors" (
1172
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1173
- "tenant_id" TEXT NOT NULL,
1174
- "account_id" TEXT NOT NULL,
1175
- "account_name" TEXT NOT NULL,
1176
- "email" TEXT,
1177
- "access_status" TEXT NOT NULL DEFAULT 'Active',
1178
- "connection_status" TEXT NOT NULL DEFAULT 'PENDING',
1179
- "connection_id" TEXT NOT NULL,
1180
- "management_account_id" TEXT,
1181
- "external_id" TEXT NOT NULL,
1182
- "role_name" TEXT NOT NULL,
1183
- "role_arn" TEXT NOT NULL,
1184
- "selected" BOOLEAN NOT NULL DEFAULT false,
1185
- "joined_method" TEXT,
1186
- "connected_at" TIMESTAMPTZ,
1187
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1188
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1189
- )
1190
- `));
1191
- await db.execute(sql.raw(`
1192
- CREATE UNIQUE INDEX IF NOT EXISTS insight_connectors_tenant_account_unique
1193
- ON "meyiconnect"."insight_connectors" ("tenant_id", "account_id")
1194
- `));
1195
- await db.execute(sql.raw(`
1196
- CREATE INDEX IF NOT EXISTS insight_connectors_tenant_idx
1197
- ON "meyiconnect"."insight_connectors" ("tenant_id")
1198
- `));
1199
- await db.execute(sql.raw(`
1200
- CREATE INDEX IF NOT EXISTS insight_connectors_connection_idx
1201
- ON "meyiconnect"."insight_connectors" ("connection_id")
1170
+ await db.execute(sql.raw(`
1171
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_connectors" (
1172
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1173
+ "tenant_id" TEXT NOT NULL,
1174
+ "account_id" TEXT NOT NULL,
1175
+ "account_name" TEXT NOT NULL,
1176
+ "email" TEXT,
1177
+ "access_status" TEXT NOT NULL DEFAULT 'Active',
1178
+ "connection_status" TEXT NOT NULL DEFAULT 'PENDING',
1179
+ "connection_id" TEXT NOT NULL,
1180
+ "management_account_id" TEXT,
1181
+ "external_id" TEXT NOT NULL,
1182
+ "role_name" TEXT NOT NULL,
1183
+ "role_arn" TEXT NOT NULL,
1184
+ "selected" BOOLEAN NOT NULL DEFAULT false,
1185
+ "joined_method" TEXT,
1186
+ "connected_at" TIMESTAMPTZ,
1187
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1188
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1189
+ )
1190
+ `));
1191
+ await db.execute(sql.raw(`
1192
+ CREATE UNIQUE INDEX IF NOT EXISTS insight_connectors_tenant_account_unique
1193
+ ON "meyiconnect"."insight_connectors" ("tenant_id", "account_id")
1194
+ `));
1195
+ await db.execute(sql.raw(`
1196
+ CREATE INDEX IF NOT EXISTS insight_connectors_tenant_idx
1197
+ ON "meyiconnect"."insight_connectors" ("tenant_id")
1198
+ `));
1199
+ await db.execute(sql.raw(`
1200
+ CREATE INDEX IF NOT EXISTS insight_connectors_connection_idx
1201
+ ON "meyiconnect"."insight_connectors" ("connection_id")
1202
1202
  `));
1203
1203
  // connectors.environment: the live table is "connectors" (created by onboarding); guard in case it
1204
1204
  // doesn't exist yet. Onboarding's migrate.ts also adds this column, so either service may run first.
1205
- await db.execute(sql.raw(`
1206
- DO $$ BEGIN
1207
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'connectors') THEN
1208
- ALTER TABLE "meyiconnect"."connectors" ADD COLUMN IF NOT EXISTS "environment" TEXT;
1209
- END IF;
1210
- END $$
1205
+ await db.execute(sql.raw(`
1206
+ DO $$ BEGIN
1207
+ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'meyiconnect' AND table_name = 'connectors') THEN
1208
+ ALTER TABLE "meyiconnect"."connectors" ADD COLUMN IF NOT EXISTS "environment" TEXT;
1209
+ END IF;
1210
+ END $$
1211
1211
  `));
1212
- await db.execute(sql.raw(`
1213
- CREATE INDEX IF NOT EXISTS insight_connectors_access_status_idx
1214
- ON "meyiconnect"."insight_connectors" ("access_status")
1212
+ await db.execute(sql.raw(`
1213
+ CREATE INDEX IF NOT EXISTS insight_connectors_access_status_idx
1214
+ ON "meyiconnect"."insight_connectors" ("access_status")
1215
1215
  `));
1216
1216
  // ─── insight_connector_sla_policies ───────────────────────────────────────
1217
1217
  // Per-account SLA override (enable flag + acknowledge/resolve limits).
1218
- await db.execute(sql.raw(`
1219
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_connector_sla_policies" (
1220
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1221
- "tenant_id" TEXT NOT NULL,
1222
- "account_id" TEXT NOT NULL,
1223
- "enabled" BOOLEAN NOT NULL DEFAULT true,
1224
- "updated_by" TEXT,
1225
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1226
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1227
- )
1228
- `));
1229
- await db.execute(sql.raw(`
1230
- CREATE UNIQUE INDEX IF NOT EXISTS insight_connector_sla_policies_tenant_account_unique
1231
- ON "meyiconnect"."insight_connector_sla_policies" ("tenant_id", "account_id")
1232
- `));
1233
- await db.execute(sql.raw(`
1234
- CREATE INDEX IF NOT EXISTS insight_connector_sla_policies_tenant_idx
1235
- ON "meyiconnect"."insight_connector_sla_policies" ("tenant_id")
1218
+ await db.execute(sql.raw(`
1219
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_connector_sla_policies" (
1220
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1221
+ "tenant_id" TEXT NOT NULL,
1222
+ "account_id" TEXT NOT NULL,
1223
+ "enabled" BOOLEAN NOT NULL DEFAULT true,
1224
+ "updated_by" TEXT,
1225
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1226
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1227
+ )
1228
+ `));
1229
+ await db.execute(sql.raw(`
1230
+ CREATE UNIQUE INDEX IF NOT EXISTS insight_connector_sla_policies_tenant_account_unique
1231
+ ON "meyiconnect"."insight_connector_sla_policies" ("tenant_id", "account_id")
1232
+ `));
1233
+ await db.execute(sql.raw(`
1234
+ CREATE INDEX IF NOT EXISTS insight_connector_sla_policies_tenant_idx
1235
+ ON "meyiconnect"."insight_connector_sla_policies" ("tenant_id")
1236
1236
  `));
1237
1237
  // ─── insight_connector_sla_targets ────────────────────────────────────────
1238
1238
  // Per-priority acknowledge/resolve limits for an account's SLA policy.
1239
- await db.execute(sql.raw(`
1240
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_connector_sla_targets" (
1241
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1242
- "tenant_id" TEXT NOT NULL,
1243
- "account_id" TEXT NOT NULL,
1244
- "incident_group" TEXT NOT NULL DEFAULT 'default',
1245
- "priority" TEXT NOT NULL,
1246
- "ack_minutes" INTEGER NOT NULL,
1247
- "resolve_minutes" INTEGER,
1248
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1249
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1250
- CONSTRAINT insight_connector_sla_targets_priority_chk CHECK (priority IN ('P1','P2','P3','P4')),
1251
- CONSTRAINT insight_connector_sla_targets_group_chk
1252
- CHECK (incident_group IN ('default','operations','security','cost')),
1253
- CONSTRAINT insight_connector_sla_targets_limits_chk
1254
- CHECK (ack_minutes BETWEEN 1 AND 43200 AND (resolve_minutes IS NULL OR (resolve_minutes >= ack_minutes AND resolve_minutes <= 43200)))
1255
- )
1256
- `));
1257
- await db.execute(sql.raw(`
1258
- CREATE INDEX IF NOT EXISTS insight_connector_sla_targets_tenant_idx
1259
- ON "meyiconnect"."insight_connector_sla_targets" ("tenant_id")
1239
+ await db.execute(sql.raw(`
1240
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_connector_sla_targets" (
1241
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1242
+ "tenant_id" TEXT NOT NULL,
1243
+ "account_id" TEXT NOT NULL,
1244
+ "incident_group" TEXT NOT NULL DEFAULT 'default',
1245
+ "priority" TEXT NOT NULL,
1246
+ "ack_minutes" INTEGER NOT NULL,
1247
+ "resolve_minutes" INTEGER,
1248
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1249
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1250
+ CONSTRAINT insight_connector_sla_targets_priority_chk CHECK (priority IN ('P1','P2','P3','P4')),
1251
+ CONSTRAINT insight_connector_sla_targets_group_chk
1252
+ CHECK (incident_group IN ('default','operations','security','cost')),
1253
+ CONSTRAINT insight_connector_sla_targets_limits_chk
1254
+ CHECK (ack_minutes BETWEEN 1 AND 43200 AND (resolve_minutes IS NULL OR (resolve_minutes >= ack_minutes AND resolve_minutes <= 43200)))
1255
+ )
1256
+ `));
1257
+ await db.execute(sql.raw(`
1258
+ CREATE INDEX IF NOT EXISTS insight_connector_sla_targets_tenant_idx
1259
+ ON "meyiconnect"."insight_connector_sla_targets" ("tenant_id")
1260
1260
  `));
1261
1261
  // SLA per incident group (insight-server 1.1.0). Upgrades a 1.0.8 table in place:
1262
1262
  // existing rows become the account default ('default' group), so evaluation is unchanged.
1263
- await db.execute(sql.raw(`
1264
- ALTER TABLE "meyiconnect"."insight_connector_sla_policies"
1265
- ADD COLUMN IF NOT EXISTS "applies_to" TEXT[] NOT NULL DEFAULT '{operations,security,cost}'::text[]
1266
- `));
1267
- await db.execute(sql.raw(`
1268
- DO $$ BEGIN
1269
- IF NOT EXISTS (
1270
- SELECT 1 FROM pg_constraint
1271
- WHERE conname = 'insight_connector_sla_policies_applies_to_chk'
1272
- AND conrelid = '"meyiconnect"."insight_connector_sla_policies"'::regclass
1273
- ) THEN
1274
- ALTER TABLE "meyiconnect"."insight_connector_sla_policies" ADD CONSTRAINT insight_connector_sla_policies_applies_to_chk
1275
- CHECK ("applies_to" <@ ARRAY['operations','security','cost']::text[]);
1276
- END IF;
1277
- END $$
1278
- `));
1279
- await db.execute(sql.raw(`
1280
- ALTER TABLE "meyiconnect"."insight_connector_sla_targets"
1281
- ADD COLUMN IF NOT EXISTS "incident_group" TEXT NOT NULL DEFAULT 'default'
1263
+ await db.execute(sql.raw(`
1264
+ ALTER TABLE "meyiconnect"."insight_connector_sla_policies"
1265
+ ADD COLUMN IF NOT EXISTS "applies_to" TEXT[] NOT NULL DEFAULT '{operations,security,cost}'::text[]
1266
+ `));
1267
+ await db.execute(sql.raw(`
1268
+ DO $$ BEGIN
1269
+ IF NOT EXISTS (
1270
+ SELECT 1 FROM pg_constraint
1271
+ WHERE conname = 'insight_connector_sla_policies_applies_to_chk'
1272
+ AND conrelid = '"meyiconnect"."insight_connector_sla_policies"'::regclass
1273
+ ) THEN
1274
+ ALTER TABLE "meyiconnect"."insight_connector_sla_policies" ADD CONSTRAINT insight_connector_sla_policies_applies_to_chk
1275
+ CHECK ("applies_to" <@ ARRAY['operations','security','cost']::text[]);
1276
+ END IF;
1277
+ END $$
1278
+ `));
1279
+ await db.execute(sql.raw(`
1280
+ ALTER TABLE "meyiconnect"."insight_connector_sla_targets"
1281
+ ADD COLUMN IF NOT EXISTS "incident_group" TEXT NOT NULL DEFAULT 'default'
1282
1282
  `));
1283
1283
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_connector_sla_targets" ALTER COLUMN "resolve_minutes" DROP NOT NULL`));
1284
- await db.execute(sql.raw(`
1285
- DO $$ BEGIN
1286
- IF NOT EXISTS (
1287
- SELECT 1 FROM pg_constraint
1288
- WHERE conname = 'insight_connector_sla_targets_group_chk'
1289
- AND conrelid = '"meyiconnect"."insight_connector_sla_targets"'::regclass
1290
- ) THEN
1291
- ALTER TABLE "meyiconnect"."insight_connector_sla_targets" ADD CONSTRAINT insight_connector_sla_targets_group_chk
1292
- CHECK ("incident_group" IN ('default','operations','security','cost'));
1293
- END IF;
1294
- -- Replace the 1.0.8 limits CHECK (resolve NOT NULL) with one that allows best effort (NULL resolve).
1295
- IF EXISTS (
1296
- SELECT 1 FROM pg_constraint
1297
- WHERE conname = 'insight_connector_sla_targets_limits_chk'
1298
- AND conrelid = '"meyiconnect"."insight_connector_sla_targets"'::regclass
1299
- AND pg_get_constraintdef(oid) NOT ILIKE '%resolve_minutes IS NULL%'
1300
- ) THEN
1301
- ALTER TABLE "meyiconnect"."insight_connector_sla_targets" DROP CONSTRAINT insight_connector_sla_targets_limits_chk;
1302
- END IF;
1303
- IF NOT EXISTS (
1304
- SELECT 1 FROM pg_constraint
1305
- WHERE conname = 'insight_connector_sla_targets_limits_chk'
1306
- AND conrelid = '"meyiconnect"."insight_connector_sla_targets"'::regclass
1307
- ) THEN
1308
- ALTER TABLE "meyiconnect"."insight_connector_sla_targets" ADD CONSTRAINT insight_connector_sla_targets_limits_chk
1309
- CHECK (ack_minutes BETWEEN 1 AND 43200 AND (resolve_minutes IS NULL OR (resolve_minutes >= ack_minutes AND resolve_minutes <= 43200)));
1310
- END IF;
1311
- END $$
1284
+ await db.execute(sql.raw(`
1285
+ DO $$ BEGIN
1286
+ IF NOT EXISTS (
1287
+ SELECT 1 FROM pg_constraint
1288
+ WHERE conname = 'insight_connector_sla_targets_group_chk'
1289
+ AND conrelid = '"meyiconnect"."insight_connector_sla_targets"'::regclass
1290
+ ) THEN
1291
+ ALTER TABLE "meyiconnect"."insight_connector_sla_targets" ADD CONSTRAINT insight_connector_sla_targets_group_chk
1292
+ CHECK ("incident_group" IN ('default','operations','security','cost'));
1293
+ END IF;
1294
+ -- Replace the 1.0.8 limits CHECK (resolve NOT NULL) with one that allows best effort (NULL resolve).
1295
+ IF EXISTS (
1296
+ SELECT 1 FROM pg_constraint
1297
+ WHERE conname = 'insight_connector_sla_targets_limits_chk'
1298
+ AND conrelid = '"meyiconnect"."insight_connector_sla_targets"'::regclass
1299
+ AND pg_get_constraintdef(oid) NOT ILIKE '%resolve_minutes IS NULL%'
1300
+ ) THEN
1301
+ ALTER TABLE "meyiconnect"."insight_connector_sla_targets" DROP CONSTRAINT insight_connector_sla_targets_limits_chk;
1302
+ END IF;
1303
+ IF NOT EXISTS (
1304
+ SELECT 1 FROM pg_constraint
1305
+ WHERE conname = 'insight_connector_sla_targets_limits_chk'
1306
+ AND conrelid = '"meyiconnect"."insight_connector_sla_targets"'::regclass
1307
+ ) THEN
1308
+ ALTER TABLE "meyiconnect"."insight_connector_sla_targets" ADD CONSTRAINT insight_connector_sla_targets_limits_chk
1309
+ CHECK (ack_minutes BETWEEN 1 AND 43200 AND (resolve_minutes IS NULL OR (resolve_minutes >= ack_minutes AND resolve_minutes <= 43200)));
1310
+ END IF;
1311
+ END $$
1312
1312
  `));
1313
1313
  // The 1.0.8 unique key (tenant, account, priority) would reject group overrides; replace it.
1314
- await db.execute(sql.raw(`
1315
- ALTER TABLE "meyiconnect"."insight_connector_sla_targets"
1316
- DROP CONSTRAINT IF EXISTS insight_connector_sla_targets_tenant_account_priority_unique
1314
+ await db.execute(sql.raw(`
1315
+ ALTER TABLE "meyiconnect"."insight_connector_sla_targets"
1316
+ DROP CONSTRAINT IF EXISTS insight_connector_sla_targets_tenant_account_priority_unique
1317
1317
  `));
1318
1318
  await db.execute(sql.raw(`DROP INDEX IF EXISTS "meyiconnect"."insight_connector_sla_targets_tenant_account_priority_unique"`));
1319
- await db.execute(sql.raw(`
1320
- CREATE UNIQUE INDEX IF NOT EXISTS insight_connector_sla_targets_tenant_account_group_priority_unique
1321
- ON "meyiconnect"."insight_connector_sla_targets" ("tenant_id", "account_id", "incident_group", "priority")
1319
+ await db.execute(sql.raw(`
1320
+ CREATE UNIQUE INDEX IF NOT EXISTS insight_connector_sla_targets_tenant_account_group_priority_unique
1321
+ ON "meyiconnect"."insight_connector_sla_targets" ("tenant_id", "account_id", "incident_group", "priority")
1322
1322
  `));
1323
1323
  // Upgrade from the single-limit policy: copy each account's limits to all four priorities (so behaviour is
1324
1324
  // unchanged), then relax the old columns. They stay in place, unread, until a later release drops them.
1325
- await db.execute(sql.raw(`
1326
- DO $$ BEGIN
1327
- IF EXISTS (
1328
- SELECT 1 FROM information_schema.columns
1329
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_connector_sla_policies' AND column_name = 'ack_minutes'
1330
- ) THEN
1331
- INSERT INTO "meyiconnect"."insight_connector_sla_targets" ("tenant_id", "account_id", "priority", "ack_minutes", "resolve_minutes")
1332
- SELECT p."tenant_id", p."account_id", v."priority", p."ack_minutes", p."resolve_minutes"
1333
- FROM "meyiconnect"."insight_connector_sla_policies" p
1334
- CROSS JOIN (VALUES ('P1'), ('P2'), ('P3'), ('P4')) AS v("priority")
1335
- WHERE p."ack_minutes" IS NOT NULL AND p."resolve_minutes" IS NOT NULL
1336
- AND NOT EXISTS (
1337
- SELECT 1 FROM "meyiconnect"."insight_connector_sla_targets" t
1338
- WHERE t."tenant_id" = p."tenant_id" AND t."account_id" = p."account_id" AND t."priority" = v."priority"
1339
- );
1340
-
1341
- ALTER TABLE "meyiconnect"."insight_connector_sla_policies" ALTER COLUMN "ack_minutes" DROP NOT NULL;
1342
- ALTER TABLE "meyiconnect"."insight_connector_sla_policies" ALTER COLUMN "resolve_minutes" DROP NOT NULL;
1343
- ALTER TABLE "meyiconnect"."insight_connector_sla_policies" DROP CONSTRAINT IF EXISTS insight_connector_sla_policies_limits_chk;
1344
- END IF;
1345
- END $$
1325
+ await db.execute(sql.raw(`
1326
+ DO $$ BEGIN
1327
+ IF EXISTS (
1328
+ SELECT 1 FROM information_schema.columns
1329
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_connector_sla_policies' AND column_name = 'ack_minutes'
1330
+ ) THEN
1331
+ INSERT INTO "meyiconnect"."insight_connector_sla_targets" ("tenant_id", "account_id", "priority", "ack_minutes", "resolve_minutes")
1332
+ SELECT p."tenant_id", p."account_id", v."priority", p."ack_minutes", p."resolve_minutes"
1333
+ FROM "meyiconnect"."insight_connector_sla_policies" p
1334
+ CROSS JOIN (VALUES ('P1'), ('P2'), ('P3'), ('P4')) AS v("priority")
1335
+ WHERE p."ack_minutes" IS NOT NULL AND p."resolve_minutes" IS NOT NULL
1336
+ AND NOT EXISTS (
1337
+ SELECT 1 FROM "meyiconnect"."insight_connector_sla_targets" t
1338
+ WHERE t."tenant_id" = p."tenant_id" AND t."account_id" = p."account_id" AND t."priority" = v."priority"
1339
+ );
1340
+
1341
+ ALTER TABLE "meyiconnect"."insight_connector_sla_policies" ALTER COLUMN "ack_minutes" DROP NOT NULL;
1342
+ ALTER TABLE "meyiconnect"."insight_connector_sla_policies" ALTER COLUMN "resolve_minutes" DROP NOT NULL;
1343
+ ALTER TABLE "meyiconnect"."insight_connector_sla_policies" DROP CONSTRAINT IF EXISTS insight_connector_sla_policies_limits_chk;
1344
+ END IF;
1345
+ END $$
1346
1346
  `));
1347
1347
  // ─── insight_job_queue ────────────────────────────────────────────────────
1348
- await db.execute(sql.raw(`
1349
- CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_job_queue" (
1350
- "id" SERIAL PRIMARY KEY,
1351
- "job_type" TEXT NOT NULL,
1352
- "payload" JSONB NOT NULL,
1353
- "status" TEXT NOT NULL DEFAULT 'pending',
1354
- "attempts" INTEGER NOT NULL DEFAULT 0,
1355
- "max_attempts" INTEGER NOT NULL DEFAULT 3,
1356
- "available_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1357
- "leased_by" TEXT,
1358
- "leased_at" TIMESTAMPTZ,
1359
- "last_error" TEXT,
1360
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1361
- "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1362
- )
1363
- `));
1364
- await db.execute(sql.raw(`
1365
- CREATE INDEX IF NOT EXISTS insight_job_queue_fetch_idx
1366
- ON "meyiconnect"."insight_job_queue" ("job_type", "status", "available_at")
1348
+ await db.execute(sql.raw(`
1349
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."insight_job_queue" (
1350
+ "id" SERIAL PRIMARY KEY,
1351
+ "job_type" TEXT NOT NULL,
1352
+ "payload" JSONB NOT NULL,
1353
+ "status" TEXT NOT NULL DEFAULT 'pending',
1354
+ "attempts" INTEGER NOT NULL DEFAULT 0,
1355
+ "max_attempts" INTEGER NOT NULL DEFAULT 3,
1356
+ "available_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1357
+ "leased_by" TEXT,
1358
+ "leased_at" TIMESTAMPTZ,
1359
+ "last_error" TEXT,
1360
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1361
+ "updated_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1362
+ )
1363
+ `));
1364
+ await db.execute(sql.raw(`
1365
+ CREATE INDEX IF NOT EXISTS insight_job_queue_fetch_idx
1366
+ ON "meyiconnect"."insight_job_queue" ("job_type", "status", "available_at")
1367
1367
  `));
1368
1368
  // ─── generic tags migration & project_tag removal ─────────────────────────
1369
- await db.execute(sql.raw(`
1370
- ALTER TABLE "meyiconnect"."insight_alerts"
1371
- ADD COLUMN IF NOT EXISTS "tags" JSONB NOT NULL DEFAULT '{}'::jsonb;
1372
- `));
1373
- await db.execute(sql.raw(`
1374
- ALTER TABLE "meyiconnect"."insight_incidents"
1375
- ADD COLUMN IF NOT EXISTS "tags" JSONB NOT NULL DEFAULT '{}'::jsonb;
1376
- `));
1377
- await db.execute(sql.raw(`
1378
- DO $$
1379
- BEGIN
1380
- IF EXISTS (
1381
- SELECT 1 FROM information_schema.columns
1382
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'project_tag'
1383
- ) THEN
1384
- UPDATE "meyiconnect"."insight_alerts"
1385
- SET "tags" = COALESCE("tags", '{}'::jsonb)
1386
- || CASE WHEN "additional_configuration"->'tags' IS NOT NULL THEN ("additional_configuration"->'tags') ELSE '{}'::jsonb END
1387
- || CASE WHEN "project_tag" IS NOT NULL AND "project_tag" != '' THEN jsonb_build_object('project', "project_tag") ELSE '{}'::jsonb END;
1388
-
1389
- DROP INDEX IF EXISTS "meyiconnect"."insight_alerts_project_tag_idx";
1390
- ALTER TABLE "meyiconnect"."insight_alerts" DROP COLUMN "project_tag";
1391
- END IF;
1392
-
1393
- IF EXISTS (
1394
- SELECT 1 FROM information_schema.columns
1395
- WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'project_tag'
1396
- ) THEN
1397
- UPDATE "meyiconnect"."insight_incidents"
1398
- SET "tags" = COALESCE("tags", '{}'::jsonb)
1399
- || CASE WHEN "project_tag" IS NOT NULL AND "project_tag" != '' THEN jsonb_build_object('project', "project_tag") ELSE '{}'::jsonb END;
1400
-
1401
- ALTER TABLE "meyiconnect"."insight_incidents" DROP COLUMN "project_tag";
1402
- END IF;
1403
- END $$;
1404
- `));
1405
- await db.execute(sql.raw(`
1406
- CREATE INDEX IF NOT EXISTS insight_alerts_tags_idx
1407
- ON "meyiconnect"."insight_alerts" USING GIN ("tags");
1408
- `));
1409
- await db.execute(sql.raw(`
1410
- CREATE INDEX IF NOT EXISTS insight_incidents_tags_idx
1411
- ON "meyiconnect"."insight_incidents" USING GIN ("tags");
1369
+ await db.execute(sql.raw(`
1370
+ ALTER TABLE "meyiconnect"."insight_alerts"
1371
+ ADD COLUMN IF NOT EXISTS "tags" JSONB NOT NULL DEFAULT '{}'::jsonb;
1372
+ `));
1373
+ await db.execute(sql.raw(`
1374
+ ALTER TABLE "meyiconnect"."insight_incidents"
1375
+ ADD COLUMN IF NOT EXISTS "tags" JSONB NOT NULL DEFAULT '{}'::jsonb;
1376
+ `));
1377
+ await db.execute(sql.raw(`
1378
+ DO $$
1379
+ BEGIN
1380
+ IF EXISTS (
1381
+ SELECT 1 FROM information_schema.columns
1382
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_alerts' AND column_name = 'project_tag'
1383
+ ) THEN
1384
+ UPDATE "meyiconnect"."insight_alerts"
1385
+ SET "tags" = COALESCE("tags", '{}'::jsonb)
1386
+ || CASE WHEN "additional_configuration"->'tags' IS NOT NULL THEN ("additional_configuration"->'tags') ELSE '{}'::jsonb END
1387
+ || CASE WHEN "project_tag" IS NOT NULL AND "project_tag" != '' THEN jsonb_build_object('project', "project_tag") ELSE '{}'::jsonb END;
1388
+
1389
+ DROP INDEX IF EXISTS "meyiconnect"."insight_alerts_project_tag_idx";
1390
+ ALTER TABLE "meyiconnect"."insight_alerts" DROP COLUMN "project_tag";
1391
+ END IF;
1392
+
1393
+ IF EXISTS (
1394
+ SELECT 1 FROM information_schema.columns
1395
+ WHERE table_schema = 'meyiconnect' AND table_name = 'insight_incidents' AND column_name = 'project_tag'
1396
+ ) THEN
1397
+ UPDATE "meyiconnect"."insight_incidents"
1398
+ SET "tags" = COALESCE("tags", '{}'::jsonb)
1399
+ || CASE WHEN "project_tag" IS NOT NULL AND "project_tag" != '' THEN jsonb_build_object('project', "project_tag") ELSE '{}'::jsonb END;
1400
+
1401
+ ALTER TABLE "meyiconnect"."insight_incidents" DROP COLUMN "project_tag";
1402
+ END IF;
1403
+ END $$;
1404
+ `));
1405
+ await db.execute(sql.raw(`
1406
+ CREATE INDEX IF NOT EXISTS insight_alerts_tags_idx
1407
+ ON "meyiconnect"."insight_alerts" USING GIN ("tags");
1408
+ `));
1409
+ await db.execute(sql.raw(`
1410
+ CREATE INDEX IF NOT EXISTS insight_incidents_tags_idx
1411
+ ON "meyiconnect"."insight_incidents" USING GIN ("tags");
1412
1412
  `));
1413
1413
  // ai_usage_log — one row per AI run (an RCA investigation, or a SOP
1414
1414
  // generation), carrying the token/cost totals insight-ai already computes
1415
1415
  // via app/costing/calculator.py but previously only logged.
1416
- await db.execute(sql.raw(`
1417
- CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_usage_log" (
1418
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1419
- "tenant_id" TEXT NOT NULL,
1420
- "call_type" TEXT NOT NULL,
1421
- "incident_id" UUID,
1422
- "incident_name" TEXT,
1423
- "sop_id" TEXT,
1424
- "agent_job_id" UUID,
1425
- "models" JSONB,
1426
- "total_input_tokens" INTEGER NOT NULL DEFAULT 0,
1427
- "total_output_tokens" INTEGER NOT NULL DEFAULT 0,
1428
- "total_cache_read_tokens" INTEGER NOT NULL DEFAULT 0,
1429
- "total_cache_write_tokens" INTEGER NOT NULL DEFAULT 0,
1430
- "total_tokens" INTEGER NOT NULL DEFAULT 0,
1431
- "total_cost" NUMERIC(18,6),
1432
- "currency" TEXT NOT NULL DEFAULT 'USD',
1433
- "priced" BOOLEAN NOT NULL DEFAULT false,
1434
- "reason" TEXT,
1435
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1436
- );
1437
- `));
1438
- await db.execute(sql.raw(`
1439
- CREATE INDEX IF NOT EXISTS ai_usage_log_tenant_id_idx
1440
- ON "meyiconnect"."ai_usage_log" ("tenant_id");
1441
- `));
1442
- await db.execute(sql.raw(`
1443
- CREATE INDEX IF NOT EXISTS ai_usage_log_incident_id_idx
1444
- ON "meyiconnect"."ai_usage_log" ("incident_id");
1416
+ await db.execute(sql.raw(`
1417
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_usage_log" (
1418
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1419
+ "tenant_id" TEXT NOT NULL,
1420
+ "call_type" TEXT NOT NULL,
1421
+ "incident_id" UUID,
1422
+ "incident_name" TEXT,
1423
+ "sop_id" TEXT,
1424
+ "agent_job_id" UUID,
1425
+ "models" JSONB,
1426
+ "total_input_tokens" INTEGER NOT NULL DEFAULT 0,
1427
+ "total_output_tokens" INTEGER NOT NULL DEFAULT 0,
1428
+ "total_cache_read_tokens" INTEGER NOT NULL DEFAULT 0,
1429
+ "total_cache_write_tokens" INTEGER NOT NULL DEFAULT 0,
1430
+ "total_tokens" INTEGER NOT NULL DEFAULT 0,
1431
+ "total_cost" NUMERIC(18,6),
1432
+ "currency" TEXT NOT NULL DEFAULT 'USD',
1433
+ "priced" BOOLEAN NOT NULL DEFAULT false,
1434
+ "reason" TEXT,
1435
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW()
1436
+ );
1437
+ `));
1438
+ await db.execute(sql.raw(`
1439
+ CREATE INDEX IF NOT EXISTS ai_usage_log_tenant_id_idx
1440
+ ON "meyiconnect"."ai_usage_log" ("tenant_id");
1441
+ `));
1442
+ await db.execute(sql.raw(`
1443
+ CREATE INDEX IF NOT EXISTS ai_usage_log_incident_id_idx
1444
+ ON "meyiconnect"."ai_usage_log" ("incident_id");
1445
1445
  `));
1446
1446
  // report_id links call_type='cost_report' rows (written by
1447
1447
  // insight-cost-server from the cost-ai-analyser's status.json) back to
1448
1448
  // meyiconnect.cost_ai_reports. insight-cost-server's schema installer adds
1449
1449
  // the same column, so either host can migrate first.
1450
- await db.execute(sql.raw(`
1451
- ALTER TABLE "meyiconnect"."ai_usage_log" ADD COLUMN IF NOT EXISTS "report_id" TEXT;
1450
+ await db.execute(sql.raw(`
1451
+ ALTER TABLE "meyiconnect"."ai_usage_log" ADD COLUMN IF NOT EXISTS "report_id" TEXT;
1452
1452
  `));
1453
1453
  // The AI usage API (src/features/ai-usage) reads a tenant's rows by time window.
1454
- await db.execute(sql.raw(`
1455
- CREATE INDEX IF NOT EXISTS ai_usage_log_tenant_created_idx
1456
- ON "meyiconnect"."ai_usage_log" ("tenant_id", "created_at" DESC);
1454
+ await db.execute(sql.raw(`
1455
+ CREATE INDEX IF NOT EXISTS ai_usage_log_tenant_created_idx
1456
+ ON "meyiconnect"."ai_usage_log" ("tenant_id", "created_at" DESC);
1457
1457
  `));
1458
1458
  // The re-pricing job (src/features/ai-pricing) scans recent unpriced rows across tenants.
1459
- await db.execute(sql.raw(`
1460
- CREATE INDEX IF NOT EXISTS ai_usage_log_unpriced_idx
1461
- ON "meyiconnect"."ai_usage_log" ("created_at", "id") WHERE "priced" = false;
1459
+ await db.execute(sql.raw(`
1460
+ CREATE INDEX IF NOT EXISTS ai_usage_log_unpriced_idx
1461
+ ON "meyiconnect"."ai_usage_log" ("created_at", "id") WHERE "priced" = false;
1462
1462
  `));
1463
1463
  // ─── AI model pricing (src/features/ai-pricing, docs: insight-ai/docs/ai-model-pricing-plan.md) ─
1464
1464
  // Platform-level list prices for every AI provider, filled daily from LiteLLM's
1465
1465
  // public price file plus manual overrides. Not tenant-scoped. insight-cost-server's
1466
1466
  // schema installer mirrors ai_model_pricing and ai_model_aliases identically (it
1467
1467
  // reads them), so either host can migrate first.
1468
- await db.execute(sql.raw(`
1469
- CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_model_pricing" (
1470
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1471
- "provider" TEXT NOT NULL,
1472
- "model_key" TEXT NOT NULL,
1473
- "region" TEXT NOT NULL DEFAULT '*',
1474
- "input_per_million" NUMERIC(18,6) NOT NULL CHECK ("input_per_million" >= 0),
1475
- "output_per_million" NUMERIC(18,6) NOT NULL CHECK ("output_per_million" >= 0),
1476
- "cache_read_per_million" NUMERIC(18,6) CHECK ("cache_read_per_million" >= 0),
1477
- "cache_write_per_million" NUMERIC(18,6) CHECK ("cache_write_per_million" >= 0),
1478
- "currency" TEXT NOT NULL DEFAULT 'USD',
1479
- "effective_from" TIMESTAMPTZ NOT NULL,
1480
- "effective_to" TIMESTAMPTZ,
1481
- "source" TEXT NOT NULL,
1482
- "source_ref" TEXT,
1483
- "verified_at" TIMESTAMPTZ,
1484
- "updated_by" TEXT,
1485
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1486
- CHECK ("effective_to" IS NULL OR "effective_to" > "effective_from")
1487
- );
1488
- `));
1489
- await db.execute(sql.raw(`
1490
- CREATE UNIQUE INDEX IF NOT EXISTS ai_model_pricing_current_uq
1491
- ON "meyiconnect"."ai_model_pricing" ("provider", "model_key", "region") WHERE "effective_to" IS NULL;
1492
- `));
1493
- await db.execute(sql.raw(`
1494
- CREATE INDEX IF NOT EXISTS ai_model_pricing_lookup_idx
1495
- ON "meyiconnect"."ai_model_pricing" ("provider", "model_key", "region", "effective_from" DESC);
1496
- `));
1497
- await db.execute(sql.raw(`
1498
- CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_model_aliases" (
1499
- "provider" TEXT NOT NULL,
1500
- "alias" TEXT NOT NULL,
1501
- "model_key" TEXT NOT NULL,
1502
- PRIMARY KEY ("provider", "alias")
1503
- );
1504
- `));
1505
- await db.execute(sql.raw(`
1506
- CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_pricing_sync_state" (
1507
- "source" TEXT PRIMARY KEY,
1508
- "source_ref" TEXT,
1509
- "etag" TEXT,
1510
- "last_checked_at" TIMESTAMPTZ,
1511
- "last_changed_at" TIMESTAMPTZ,
1512
- "last_repriced_at" TIMESTAMPTZ,
1513
- "last_error" TEXT
1514
- );
1515
- `));
1516
- await db.execute(sql.raw(`
1517
- CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_model_pricing_review" (
1518
- "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1519
- "provider" TEXT NOT NULL,
1520
- "model_key" TEXT NOT NULL,
1521
- "region" TEXT NOT NULL,
1522
- "current" JSONB,
1523
- "proposed" JSONB NOT NULL,
1524
- "reason" TEXT NOT NULL,
1525
- "status" TEXT NOT NULL DEFAULT 'pending' CHECK ("status" IN ('pending','applied','rejected')),
1526
- "source_ref" TEXT,
1527
- "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1528
- "decided_by" TEXT,
1529
- "decided_at" TIMESTAMPTZ
1530
- );
1531
- `));
1532
- await db.execute(sql.raw(`
1533
- CREATE INDEX IF NOT EXISTS ai_model_pricing_review_pending_idx
1534
- ON "meyiconnect"."ai_model_pricing_review" ("provider", "model_key", "region") WHERE "status" = 'pending';
1468
+ await db.execute(sql.raw(`
1469
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_model_pricing" (
1470
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1471
+ "provider" TEXT NOT NULL,
1472
+ "model_key" TEXT NOT NULL,
1473
+ "region" TEXT NOT NULL DEFAULT '*',
1474
+ "input_per_million" NUMERIC(18,6) NOT NULL CHECK ("input_per_million" >= 0),
1475
+ "output_per_million" NUMERIC(18,6) NOT NULL CHECK ("output_per_million" >= 0),
1476
+ "cache_read_per_million" NUMERIC(18,6) CHECK ("cache_read_per_million" >= 0),
1477
+ "cache_write_per_million" NUMERIC(18,6) CHECK ("cache_write_per_million" >= 0),
1478
+ "currency" TEXT NOT NULL DEFAULT 'USD',
1479
+ "effective_from" TIMESTAMPTZ NOT NULL,
1480
+ "effective_to" TIMESTAMPTZ,
1481
+ "source" TEXT NOT NULL,
1482
+ "source_ref" TEXT,
1483
+ "verified_at" TIMESTAMPTZ,
1484
+ "updated_by" TEXT,
1485
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1486
+ CHECK ("effective_to" IS NULL OR "effective_to" > "effective_from")
1487
+ );
1488
+ `));
1489
+ await db.execute(sql.raw(`
1490
+ CREATE UNIQUE INDEX IF NOT EXISTS ai_model_pricing_current_uq
1491
+ ON "meyiconnect"."ai_model_pricing" ("provider", "model_key", "region") WHERE "effective_to" IS NULL;
1492
+ `));
1493
+ await db.execute(sql.raw(`
1494
+ CREATE INDEX IF NOT EXISTS ai_model_pricing_lookup_idx
1495
+ ON "meyiconnect"."ai_model_pricing" ("provider", "model_key", "region", "effective_from" DESC);
1496
+ `));
1497
+ await db.execute(sql.raw(`
1498
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_model_aliases" (
1499
+ "provider" TEXT NOT NULL,
1500
+ "alias" TEXT NOT NULL,
1501
+ "model_key" TEXT NOT NULL,
1502
+ PRIMARY KEY ("provider", "alias")
1503
+ );
1504
+ `));
1505
+ await db.execute(sql.raw(`
1506
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_pricing_sync_state" (
1507
+ "source" TEXT PRIMARY KEY,
1508
+ "source_ref" TEXT,
1509
+ "etag" TEXT,
1510
+ "last_checked_at" TIMESTAMPTZ,
1511
+ "last_changed_at" TIMESTAMPTZ,
1512
+ "last_repriced_at" TIMESTAMPTZ,
1513
+ "last_error" TEXT
1514
+ );
1515
+ `));
1516
+ await db.execute(sql.raw(`
1517
+ CREATE TABLE IF NOT EXISTS "meyiconnect"."ai_model_pricing_review" (
1518
+ "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
1519
+ "provider" TEXT NOT NULL,
1520
+ "model_key" TEXT NOT NULL,
1521
+ "region" TEXT NOT NULL,
1522
+ "current" JSONB,
1523
+ "proposed" JSONB NOT NULL,
1524
+ "reason" TEXT NOT NULL,
1525
+ "status" TEXT NOT NULL DEFAULT 'pending' CHECK ("status" IN ('pending','applied','rejected')),
1526
+ "source_ref" TEXT,
1527
+ "created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
1528
+ "decided_by" TEXT,
1529
+ "decided_at" TIMESTAMPTZ
1530
+ );
1531
+ `));
1532
+ await db.execute(sql.raw(`
1533
+ CREATE INDEX IF NOT EXISTS ai_model_pricing_review_pending_idx
1534
+ ON "meyiconnect"."ai_model_pricing_review" ("provider", "model_key", "region") WHERE "status" = 'pending';
1535
1535
  `));
1536
1536
  // ─── insight_incidents.priority ───────────────────────────────────────────
1537
1537
  // P1..P4. The default is set before the backfill so rows inserted meanwhile (e.g. by a server that
@@ -1539,34 +1539,34 @@ export async function runMigrations(db) {
1539
1539
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "priority" TEXT`));
1540
1540
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "priority" SET DEFAULT 'P2'`));
1541
1541
  // Backfill: priority tag, else severity tag, else the linked alert's severity, else P2.
1542
- await db.execute(sql.raw(`
1543
- UPDATE "meyiconnect"."insight_incidents" i
1544
- SET "priority" = COALESCE(
1545
- CASE UPPER(BTRIM(i."tags"->>'priority'))
1546
- WHEN 'P1' THEN 'P1' WHEN 'P2' THEN 'P2' WHEN 'P3' THEN 'P3' WHEN 'P4' THEN 'P4'
1547
- END,
1548
- CASE LOWER(BTRIM(i."tags"->>'severity'))
1549
- WHEN 'critical' THEN 'P1' WHEN 'high' THEN 'P2' WHEN 'medium' THEN 'P3' WHEN 'low' THEN 'P4' WHEN 'info' THEN 'P4'
1550
- END,
1551
- CASE LOWER(BTRIM((SELECT a."severity" FROM "meyiconnect"."insight_alerts" a WHERE a."id" = i."source_alert_id")))
1552
- WHEN 'critical' THEN 'P1' WHEN 'high' THEN 'P2' WHEN 'medium' THEN 'P3' WHEN 'low' THEN 'P4' WHEN 'info' THEN 'P4'
1553
- END,
1554
- 'P2'
1555
- )
1556
- WHERE i."priority" IS NULL
1542
+ await db.execute(sql.raw(`
1543
+ UPDATE "meyiconnect"."insight_incidents" i
1544
+ SET "priority" = COALESCE(
1545
+ CASE UPPER(BTRIM(i."tags"->>'priority'))
1546
+ WHEN 'P1' THEN 'P1' WHEN 'P2' THEN 'P2' WHEN 'P3' THEN 'P3' WHEN 'P4' THEN 'P4'
1547
+ END,
1548
+ CASE LOWER(BTRIM(i."tags"->>'severity'))
1549
+ WHEN 'critical' THEN 'P1' WHEN 'high' THEN 'P2' WHEN 'medium' THEN 'P3' WHEN 'low' THEN 'P4' WHEN 'info' THEN 'P4'
1550
+ END,
1551
+ CASE LOWER(BTRIM((SELECT a."severity" FROM "meyiconnect"."insight_alerts" a WHERE a."id" = i."source_alert_id")))
1552
+ WHEN 'critical' THEN 'P1' WHEN 'high' THEN 'P2' WHEN 'medium' THEN 'P3' WHEN 'low' THEN 'P4' WHEN 'info' THEN 'P4'
1553
+ END,
1554
+ 'P2'
1555
+ )
1556
+ WHERE i."priority" IS NULL
1557
1557
  `));
1558
1558
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ALTER COLUMN "priority" SET NOT NULL`));
1559
- await db.execute(sql.raw(`
1560
- DO $$ BEGIN
1561
- IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'insight_incidents_priority_chk') THEN
1562
- ALTER TABLE "meyiconnect"."insight_incidents" ADD CONSTRAINT insight_incidents_priority_chk
1563
- CHECK ("priority" IN ('P1','P2','P3','P4'));
1564
- END IF;
1565
- END $$
1566
- `));
1567
- await db.execute(sql.raw(`
1568
- CREATE INDEX IF NOT EXISTS insight_incidents_tenant_priority_idx
1569
- ON "meyiconnect"."insight_incidents" ("tenant_id", "priority")
1559
+ await db.execute(sql.raw(`
1560
+ DO $$ BEGIN
1561
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'insight_incidents_priority_chk') THEN
1562
+ ALTER TABLE "meyiconnect"."insight_incidents" ADD CONSTRAINT insight_incidents_priority_chk
1563
+ CHECK ("priority" IN ('P1','P2','P3','P4'));
1564
+ END IF;
1565
+ END $$
1566
+ `));
1567
+ await db.execute(sql.raw(`
1568
+ CREATE INDEX IF NOT EXISTS insight_incidents_tenant_priority_idx
1569
+ ON "meyiconnect"."insight_incidents" ("tenant_id", "priority")
1570
1570
  `));
1571
1571
  // ─── insight_incidents: structured cost data (1.1.0) ──────────────────────
1572
1572
  // external_ref: stable producer key; savings_monthly: sort key (USD/month); savings_detail: display range.
@@ -1574,29 +1574,72 @@ export async function runMigrations(db) {
1574
1574
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "savings_monthly" NUMERIC(14,2)`));
1575
1575
  await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "savings_detail" JSONB`));
1576
1576
  // One OPEN incident per (tenant, source, external_ref). A resolved/closed one leaves room for a new incident.
1577
- await createIndexConcurrently(db, "insight_incidents_tenant_source_ref_uq", `
1578
- CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS insight_incidents_tenant_source_ref_uq
1579
- ON "meyiconnect"."insight_incidents" ("tenant_id", "source", "external_ref")
1580
- WHERE "external_ref" IS NOT NULL AND "status" NOT IN ('resolved','closed')
1577
+ await createIndexConcurrently(db, "insight_incidents_tenant_source_ref_uq", `
1578
+ CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS insight_incidents_tenant_source_ref_uq
1579
+ ON "meyiconnect"."insight_incidents" ("tenant_id", "source", "external_ref")
1580
+ WHERE "external_ref" IS NOT NULL AND "status" NOT IN ('resolved','closed')
1581
1581
  `);
1582
1582
  // Cost list order: priority, savings, newest.
1583
- await createIndexConcurrently(db, "insight_incidents_cost_sort_idx", `
1584
- CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_incidents_cost_sort_idx
1585
- ON "meyiconnect"."insight_incidents" ("tenant_id", "priority", "savings_monthly" DESC NULLS LAST, "created_at" DESC)
1583
+ await createIndexConcurrently(db, "insight_incidents_cost_sort_idx", `
1584
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_incidents_cost_sort_idx
1585
+ ON "meyiconnect"."insight_incidents" ("tenant_id", "priority", "savings_monthly" DESC NULLS LAST, "created_at" DESC)
1586
1586
  `);
1587
+ // ─── Incident lifecycle timestamps (1.1.8) ────────────────────────────────
1588
+ // resolved_at was never written before 1.1.8, so "Resolved today", MTTR and the resolve SLA had no data.
1589
+ // Both backfills take the status_change event when there is one, else the proxy the SLA evaluator already used.
1590
+ // They touch only rows still null, so after the first boot they find nothing.
1591
+ await db.execute(sql.raw(`ALTER TABLE "meyiconnect"."insight_incidents" ADD COLUMN IF NOT EXISTS "acked_at" TIMESTAMPTZ`));
1592
+ await db.execute(sql.raw(`
1593
+ UPDATE "meyiconnect"."insight_incidents" i
1594
+ SET resolved_at = COALESCE((
1595
+ SELECT max(e.created_at) FROM "meyiconnect"."incident_events" e
1596
+ WHERE e.incident_id = i.id AND e.type = 'status_change' AND e.after_value IN ('resolved', 'closed')
1597
+ ), i.updated_at)
1598
+ WHERE i.resolved_at IS NULL AND i.status IN ('resolved', 'closed')
1599
+ `));
1600
+ await db.execute(sql.raw(`
1601
+ UPDATE "meyiconnect"."insight_incidents" i
1602
+ SET acked_at = COALESCE((
1603
+ SELECT min(e.created_at) FROM "meyiconnect"."incident_events" e
1604
+ WHERE e.incident_id = i.id AND e.type = 'status_change' AND e.before_value = 'open'
1605
+ ), i.analysis_triggered_at, i.updated_at)
1606
+ WHERE i.acked_at IS NULL AND i.status <> 'open'
1607
+ `));
1608
+ // ─── CloudTrail deployment status (1.1.8) ─────────────────────────────────
1609
+ // The stack's status callback was rejected (403) before 1.1.8, so sources stayed not_started while sending
1610
+ // alerts. A source that has sent alerts is deployed.
1611
+ await db.execute(sql.raw(`
1612
+ UPDATE "meyiconnect"."insight_cloudtrail_source_setups" s
1613
+ SET aws_deployment_status = 'deployed',
1614
+ aws_deployment_status_reason = 'Alerts received from this source',
1615
+ updated_at = now()
1616
+ WHERE s.aws_deployment_status IN ('not_started', 'pending')
1617
+ AND EXISTS (SELECT 1 FROM "meyiconnect"."insight_alerts" a WHERE a.source_id = s.source_id)
1618
+ `));
1619
+ // ─── Alert account names (1.1.8) ──────────────────────────────────────────
1620
+ // Alerts from AWS payloads were stored with the account id only; take the name from the tenant's connector.
1621
+ await db.execute(sql.raw(`
1622
+ UPDATE "meyiconnect"."insight_alerts" a
1623
+ SET account_name = c.account_name
1624
+ FROM "meyiconnect"."connectors" c
1625
+ WHERE a.account_name IS NULL
1626
+ AND a.account_id IS NOT NULL
1627
+ AND c.account_id = a.account_id
1628
+ AND c.tenant_id::text = a.tenant_id::text
1629
+ `));
1587
1630
  // ─── Retention indexes (1.1.0) ────────────────────────────────────────────
1588
1631
  // Support the tenant-scoped, time-bounded batched purge. Built CONCURRENTLY so large tables stay writable.
1589
- await createIndexConcurrently(db, "insight_alerts_tenant_created_idx", `
1590
- CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_alerts_tenant_created_idx
1591
- ON "meyiconnect"."insight_alerts" ("tenant_id", "created_at")
1632
+ await createIndexConcurrently(db, "insight_alerts_tenant_created_idx", `
1633
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_alerts_tenant_created_idx
1634
+ ON "meyiconnect"."insight_alerts" ("tenant_id", "created_at")
1592
1635
  `);
1593
- await createIndexConcurrently(db, "insight_incidents_tenant_status_resolved_idx", `
1594
- CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_incidents_tenant_status_resolved_idx
1595
- ON "meyiconnect"."insight_incidents" ("tenant_id", "status", "resolved_at")
1636
+ await createIndexConcurrently(db, "insight_incidents_tenant_status_resolved_idx", `
1637
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_incidents_tenant_status_resolved_idx
1638
+ ON "meyiconnect"."insight_incidents" ("tenant_id", "status", "resolved_at")
1596
1639
  `);
1597
- await createIndexConcurrently(db, "insight_workflow_executions_tenant_triggered_idx", `
1598
- CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_workflow_executions_tenant_triggered_idx
1599
- ON "meyiconnect"."insight_workflow_executions" ("tenant_id", "triggered_at")
1640
+ await createIndexConcurrently(db, "insight_workflow_executions_tenant_triggered_idx", `
1641
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS insight_workflow_executions_tenant_triggered_idx
1642
+ ON "meyiconnect"."insight_workflow_executions" ("tenant_id", "triggered_at")
1600
1643
  `);
1601
1644
  }
1602
1645
  function rowsOf(res) {
@@ -1609,12 +1652,12 @@ function rowsOf(res) {
1609
1652
  * transaction (where CONCURRENTLY is not allowed), it falls back to a plain CREATE INDEX.
1610
1653
  */
1611
1654
  async function createIndexConcurrently(db, name, concurrentDdl) {
1612
- const lookup = await db.execute(sql `
1613
- SELECT i.indisvalid AS valid
1614
- FROM pg_class c
1615
- JOIN pg_index i ON i.indexrelid = c.oid
1616
- JOIN pg_namespace n ON n.oid = c.relnamespace
1617
- WHERE n.nspname = 'meyiconnect' AND c.relname = ${name}
1655
+ const lookup = await db.execute(sql `
1656
+ SELECT i.indisvalid AS valid
1657
+ FROM pg_class c
1658
+ JOIN pg_index i ON i.indexrelid = c.oid
1659
+ JOIN pg_namespace n ON n.oid = c.relnamespace
1660
+ WHERE n.nspname = 'meyiconnect' AND c.relname = ${name}
1618
1661
  `);
1619
1662
  const existing = rowsOf(lookup)[0];
1620
1663
  if (existing && (existing.valid === true || existing.valid === "t"))