@meyicloud/meyi-cost-server 1.8.0 → 1.8.2

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.2",
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
  }
@@ -175,6 +175,7 @@ export class CostAnalyserService {
175
175
  if (!athena.enabled) throw Object.assign(new Error("CUR discovery has not produced a queryable Athena table for this tenant"), { name: "CurDataNotReadyError", statusCode: 503 });
176
176
  const targetRoleArn = required(customer.meta?.payerRoleArn, "customer TARGET_ROLE_ARN");
177
177
  const targetAccountId = required(customer.meta?.managementAccountId || customer.accounts?.[0]?.id, "customer management account ID");
178
+ const targetAccountName = customer.accounts?.find((account) => account.id === targetAccountId)?.name || targetAccountId;
178
179
  const prefix = `tenants/${tenantId}/reports/${reportId}`;
179
180
  let providerSecretArn;
180
181
  try {
@@ -189,6 +190,7 @@ export class CostAnalyserService {
189
190
  REPORT_BUCKET: this.reportBucket,
190
191
  REPORT_BUCKET_REGION: this.region,
191
192
  TARGET_ACCOUNT_ID: targetAccountId,
193
+ TARGET_ACCOUNT_NAME: targetAccountName,
192
194
  TARGET_ROLE_ARN: targetRoleArn,
193
195
  TARGET_ROLE_EXTERNAL_ID: customer.meta?.externalId,
194
196
  TARGET_REGIONS: this.targetRegions,
@@ -17,22 +17,15 @@ export class CostIncidentSyncService {
17
17
  if (!this.incidentSink) return false;
18
18
  try {
19
19
  const key = this.ticketsKey(report);
20
- if (!report.artifact_bucket || !key) throw new Error("FinOps ticket artifact location is missing");
20
+ if (!report.artifact_bucket || !key) throw new Error("Cost ticket artifact location is missing");
21
21
  const data = await this.artifactService.get(report.artifact_bucket, key);
22
- if (!data) throw new Error("FinOps ticket artifact is empty");
22
+ if (!data) throw new Error("Cost 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");