@meyicloud/meyi-cost-server 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meyicloud/meyi-cost-server",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "Tenant-aware AWS CUR and Athena cost plugin server for MeyiConnect",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -7,7 +7,6 @@ export class CostAnalysisReportRepository {
7
7
  this.db = db;
8
8
  this.schedules = `${qSchema}.cost_ai_report_schedules`;
9
9
  this.reports = `${qSchema}.cost_ai_reports`;
10
- this.incidentTickets = `${qSchema}.cost_ai_incident_tickets`;
11
10
  }
12
11
 
13
12
  async getSchedule(tenantId) {
@@ -110,43 +109,6 @@ export class CostAnalysisReportRepository {
110
109
  `));
111
110
  }
112
111
 
113
- async stageIncidentTickets(report, tickets) {
114
- for (const ticket of tickets) {
115
- await this.db.execute(sql`
116
- INSERT INTO ${sql.raw(this.incidentTickets)}
117
- (tenant_id, ticket_id, report_id, payload)
118
- VALUES (${report.tenant_id}, ${ticket.ticket_id}, ${report.id}, ${JSON.stringify(ticket)}::jsonb)
119
- ON CONFLICT (tenant_id, ticket_id) DO UPDATE SET
120
- payload = EXCLUDED.payload, report_id = EXCLUDED.report_id, updated_at = now()
121
- `);
122
- }
123
- }
124
-
125
- async listUnsyncedIncidentTickets(reportId) {
126
- return rows(await this.db.execute(sql`
127
- SELECT * FROM ${sql.raw(this.incidentTickets)}
128
- WHERE report_id = ${reportId} AND sync_status IN ('PENDING', 'FAILED')
129
- ORDER BY created_at ASC
130
- `));
131
- }
132
-
133
- async markIncidentTicketSynced(tenantId, ticketId, incidentId) {
134
- await this.db.execute(sql`
135
- UPDATE ${sql.raw(this.incidentTickets)}
136
- SET incident_id = ${incidentId}, sync_status = 'COMPLETED', sync_error = NULL,
137
- synced_at = now(), updated_at = now()
138
- WHERE tenant_id = ${tenantId} AND ticket_id = ${ticketId}
139
- `);
140
- }
141
-
142
- async markIncidentTicketFailed(tenantId, ticketId, error) {
143
- await this.db.execute(sql`
144
- UPDATE ${sql.raw(this.incidentTickets)}
145
- SET sync_status = 'FAILED', sync_error = ${String(error?.message || error).slice(0, 2000)}, updated_at = now()
146
- WHERE tenant_id = ${tenantId} AND ticket_id = ${ticketId}
147
- `);
148
- }
149
-
150
112
  async setIncidentSyncState(reportId, status, error = null) {
151
113
  await this.db.execute(sql`
152
114
  UPDATE ${sql.raw(this.reports)}
@@ -67,18 +67,5 @@ export async function installCostAnalysisReportSchema(db, qSchema) {
67
67
  await db.execute(sql.raw(`UPDATE ${qSchema}.cost_ai_reports
68
68
  SET ticket_count = (result->>'ticketCount')::integer
69
69
  WHERE ticket_count = 0 AND result ? 'ticketCount' AND (result->>'ticketCount') ~ '^[0-9]+$'`));
70
- await db.execute(sql.raw(`CREATE TABLE IF NOT EXISTS ${qSchema}.cost_ai_incident_tickets (
71
- tenant_id text NOT NULL,
72
- ticket_id text NOT NULL,
73
- report_id text NOT NULL REFERENCES ${qSchema}.cost_ai_reports(id) ON DELETE CASCADE,
74
- payload jsonb NOT NULL,
75
- incident_id text,
76
- sync_status text NOT NULL DEFAULT 'PENDING' CHECK (sync_status IN ('PENDING', 'COMPLETED', 'FAILED')),
77
- sync_error text,
78
- synced_at timestamptz,
79
- created_at timestamptz NOT NULL DEFAULT now(),
80
- updated_at timestamptz NOT NULL DEFAULT now(),
81
- PRIMARY KEY (tenant_id, ticket_id)
82
- )`));
83
- await db.execute(sql.raw(`CREATE INDEX IF NOT EXISTS cost_ai_incident_tickets_report_idx ON ${qSchema}.cost_ai_incident_tickets (report_id, sync_status)`));
70
+ await db.execute(sql.raw(`DROP TABLE IF EXISTS ${qSchema}.cost_ai_incident_tickets`));
84
71
  }
@@ -22,17 +22,10 @@ export class CostIncidentSyncService {
22
22
  if (!data) throw new Error("FinOps ticket artifact is empty");
23
23
  const parsed = JSON.parse(data.toString("utf8"));
24
24
  const tickets = Array.isArray(parsed?.tickets) ? parsed.tickets.filter((ticket) => ticket?.ticket_id) : [];
25
- await this.repository.stageIncidentTickets(report, tickets);
26
25
 
27
- for (const row of await this.repository.listUnsyncedIncidentTickets(report.id)) {
28
- try {
29
- const result = await this.incidentSink({ tenantId: report.tenant_id, report, ticket: row.payload });
30
- if (!result?.id) throw new Error("Incident integration did not return an incident ID");
31
- await this.repository.markIncidentTicketSynced(report.tenant_id, row.ticket_id, result.id);
32
- } catch (error) {
33
- await this.repository.markIncidentTicketFailed(report.tenant_id, row.ticket_id, error);
34
- throw error;
35
- }
26
+ for (const ticket of tickets) {
27
+ const result = await this.incidentSink({ tenantId: report.tenant_id, report, ticket });
28
+ if (!result?.id) throw new Error(`Incident integration did not return an incident ID for ${ticket.ticket_id}`);
36
29
  }
37
30
 
38
31
  await this.repository.setIncidentSyncState(report.id, "COMPLETED");