From 2ee39b39bfb61900727f19a1f6eba293bc006186 Mon Sep 17 00:00:00 2001 From: aswincosq Date: Fri, 11 Jul 2025 09:56:31 +0530 Subject: [PATCH 1/2] Squashed commit of the following: commit fa7791e2039954685cd34d9b99e59bb3554ebe6e Author: aswincosq Date: Tue Jul 8 14:15:43 2025 +0530 Added invoice amount to /invoices endpoint commit 9fb76e727768ecaa7426adbda1a178d3187cab2d Author: aswincosq Date: Tue May 20 13:34:32 2025 +0530 Made changes in ec2 invoice to return the appropriate usage type --- package-lock.json | 105 ++++++++++++++++++++++++++++++---------------- package.json | 3 +- queries.js | 59 ++++++++++++++++---------- 3 files changed, 106 insertions(+), 61 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2780db6..7a92a1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,9 @@ "@aws-sdk/client-athena": "^3.699.0", "@aws-sdk/client-s3": "^3.701.0", "@aws-sdk/credential-providers": "^3.699.0", + "awsmetrics": "file:", "dotenv": "^16.4.5", - "fastify": "^5.1.0", + "fastify": "^5.3.0", "fastify-plugin": "^5.0.1", "sequelize": "^6.37.5", "sqlite3": "^5.1.7" @@ -1053,6 +1054,12 @@ "fast-json-stringify": "^6.0.0" } }, + "node_modules/@fastify/forwarded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz", + "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", + "license": "MIT" + }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -1061,6 +1068,16 @@ "fast-deep-equal": "^3.1.3" } }, + "node_modules/@fastify/proxy-addr": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", + "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -1896,6 +1913,10 @@ "fastq": "^1.17.1" } }, + "node_modules/awsmetrics": { + "resolved": "", + "link": true + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2238,9 +2259,9 @@ } }, "node_modules/fastify": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.2.0.tgz", - "integrity": "sha512-3s+Qt5S14Eq5dCpnE0FxTp3z4xKChI83ZnMv+k0FwX+VUoZrgCFoLAxpfdi/vT4y6Mk+g7aAMt9pgXDoZmkefQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.3.2.tgz", + "integrity": "sha512-AIPqBgtqBAwkOkrnwesEE+dOyU30dQ4kh7udxeGVR05CRGwubZx+p2H8P0C4cRnQT0+EPK4VGea2DTL2RtWttg==", "funding": [ { "type": "github", @@ -2251,20 +2272,21 @@ "url": "https://opencollective.com/fastify" } ], + "license": "MIT", "dependencies": { "@fastify/ajv-compiler": "^4.0.0", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.0.0", - "process-warning": "^4.0.0", - "proxy-addr": "^2.0.7", + "process-warning": "^5.0.0", "rfdc": "^1.3.1", - "secure-json-parse": "^3.0.1", + "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } @@ -2274,6 +2296,22 @@ "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==" }, + "node_modules/fastify/node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2300,14 +2338,6 @@ "node": ">=14" } }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -2528,11 +2558,12 @@ } }, "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">= 10" } }, "node_modules/is-fullwidth-code-point": { @@ -3001,18 +3032,6 @@ "node": ">=10" } }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -3164,9 +3183,20 @@ "optional": true }, "node_modules/secure-json-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-3.0.1.tgz", - "integrity": "sha512-9QR7G96th4QJ2+dJwvZB+JoXyt8PN+DbEjOr6kL2/JU4KH8Eb2sFdU+gt8EDdzWDWoWH0uocDdfCoFzdVSixUA==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz", + "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/semver": { "version": "7.6.3", @@ -3475,9 +3505,10 @@ } }, "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", diff --git a/package.json b/package.json index 8681538..f55584e 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,9 @@ "@aws-sdk/client-athena": "^3.699.0", "@aws-sdk/client-s3": "^3.701.0", "@aws-sdk/credential-providers": "^3.699.0", + "awsmetrics": "file:", "dotenv": "^16.4.5", - "fastify": "^5.1.0", + "fastify": "^5.3.0", "fastify-plugin": "^5.0.1", "sequelize": "^6.37.5", "sqlite3": "^5.1.7" diff --git a/queries.js b/queries.js index a826c00..1a4f485 100644 --- a/queries.js +++ b/queries.js @@ -186,8 +186,13 @@ WHERE LOWER(line_item_product_code) = LOWER('%productCode%') export const invoices = `select DISTINCT bill_invoice_id as invoiceId, - year, month -FROM ${process.env.ATHENA_CU_TABLE}`; + year, month, + SUM(line_item_unblended_cost) AS totalUnblendedCost, + SUM(line_item_blended_cost) AS totalBlendedCost +FROM ${process.env.ATHENA_CU_TABLE} +WHERE bill_invoice_id IS NOT NULL +GROUP BY bill_invoice_id, year, month +ORDER BY year DESC, month DESC;` export const invoiceById = `select DISTINCT bill_invoice_id as invoiceId, @@ -217,27 +222,35 @@ FROM ${process.env.ATHENA_CU_TABLE} WHERE bill_invoice_id = '%invoiceId%' AND LOWER(line_item_product_code) = LOWER('%productCode%');`; -export const invoiceByProductCodeUsage = `select DISTINCT - bill_invoice_id as invoiceId, - year, month, - line_item_product_code as productCode, - line_item_usage_account_id as accountId, - line_item_resource_id as resourceId, - line_item_usage_type AS usageType, - line_item_usage_amount AS usageAmount, - line_item_unblended_rate AS unblendedRate, - line_item_unblended_cost AS unblendedCost, - line_item_blended_rate AS blendedRate, - line_item_blended_cost AS blendedCost, - pricing_term AS pricingTerm, - pricing_unit AS pricingUnit, - pricing_rate_code AS pricingRateCode, - pricing_currency AS pricingCurrency, - line_item_usage_start_date AS startDate, - line_item_usage_end_date AS endDate -FROM ${process.env.ATHENA_CU_TABLE} -WHERE bill_invoice_id = '%invoiceId%' - AND LOWER(line_item_product_code) = LOWER('%productCode%');`; +export const invoiceByProductCodeUsage = ` + SELECT DISTINCT + bill_invoice_id as invoiceId, + year, month, + line_item_product_code as productCode, + line_item_usage_account_id as accountId, + line_item_resource_id as resourceId, + line_item_usage_type AS usageType, + line_item_usage_amount AS usageAmount, + line_item_unblended_rate AS unblendedRate, + line_item_unblended_cost AS unblendedCost, + line_item_blended_rate AS blendedRate, + line_item_blended_cost AS blendedCost, + pricing_term AS pricingTerm, + pricing_unit AS pricingUnit, + pricing_rate_code AS pricingRateCode, + pricing_currency AS pricingCurrency, + line_item_usage_start_date AS startDate, + line_item_usage_end_date AS endDate, + CASE + WHEN line_item_usage_type LIKE '%BoxUsage:%' + THEN SUBSTRING(line_item_usage_type, POSITION('BoxUsage:' IN line_item_usage_type) + 9) + ELSE NULL + END AS instanceType + FROM ${process.env.ATHENA_CU_TABLE} + WHERE bill_invoice_id = '%invoiceId%' + AND LOWER(line_item_product_code) = LOWER('%productCode%') + AND line_item_usage_type LIKE '%BoxUsage%' + `; export const invoiceByIdAccounts = `select DISTINCT bill_invoice_id as invoiceId, From 21d078ed37cf15f795eb57e8f3ce31f834fbc6ff Mon Sep 17 00:00:00 2001 From: aswincosq Date: Mon, 21 Jul 2025 12:10:30 +0530 Subject: [PATCH 2/2] Added few new queries for dashboard --- package-lock.json | 66 ++++++++++- package.json | 2 + queries/dashboard.js | 187 +++++++++++++++++++++++++++++++ queries.js => queries/queries.js | 0 server.js | 110 +++++++++++++++++- 5 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 queries/dashboard.js rename queries.js => queries/queries.js (100%) diff --git a/package-lock.json b/package-lock.json index 7a92a1c..9bebc38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,9 @@ "@aws-sdk/client-athena": "^3.699.0", "@aws-sdk/client-s3": "^3.701.0", "@aws-sdk/credential-providers": "^3.699.0", + "@fastify/cors": "^11.0.1", "awsmetrics": "file:", + "cors": "^2.8.5", "dotenv": "^16.4.5", "fastify": "^5.3.0", "fastify-plugin": "^5.0.1", @@ -1041,6 +1043,26 @@ "fast-uri": "^3.0.0" } }, + "node_modules/@fastify/cors": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.0.1.tgz", + "integrity": "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "toad-cache": "^3.7.0" + } + }, "node_modules/@fastify/error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz", @@ -1966,9 +1988,10 @@ "integrity": "sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA==" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", "optional": true, "dependencies": { "balanced-match": "^1.0.0", @@ -2073,6 +2096,19 @@ "node": ">=18" } }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2904,6 +2940,15 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -3505,9 +3550,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -3629,6 +3674,15 @@ "node": ">= 0.10" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index f55584e..6ca216d 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "@aws-sdk/client-athena": "^3.699.0", "@aws-sdk/client-s3": "^3.701.0", "@aws-sdk/credential-providers": "^3.699.0", + "@fastify/cors": "^11.0.1", "awsmetrics": "file:", + "cors": "^2.8.5", "dotenv": "^16.4.5", "fastify": "^5.3.0", "fastify-plugin": "^5.0.1", diff --git a/queries/dashboard.js b/queries/dashboard.js new file mode 100644 index 0000000..ae2c988 --- /dev/null +++ b/queries/dashboard.js @@ -0,0 +1,187 @@ +export const currentMonthSummary = ` + SELECT + SUM(line_item_blended_cost) as totalBlendedCost, + SUM(line_item_unblended_cost) as totalUnblendedCost, + COUNT(DISTINCT line_item_usage_account_id) as accountCount, + COUNT(DISTINCT line_item_product_code) as serviceCount, + COUNT(DISTINCT bill_invoice_id) as invoiceCount, + CONCAT(year, '-', LPAD(month, 2, '0')) as month + FROM ${process.env.ATHENA_CU_TABLE} + WHERE year = CAST(YEAR(CURRENT_DATE) AS VARCHAR) + AND month = CAST(MONTH(CURRENT_DATE) AS VARCHAR) + AND line_item_blended_cost > 0 + GROUP BY year, month +`; + +export const previousMonthSummary = ` + SELECT + SUM(line_item_blended_cost) as totalBlendedCost, + CONCAT(year, '-', LPAD(month, 2, '0')) as month + FROM ${process.env.ATHENA_CU_TABLE} + WHERE year = CAST(YEAR(DATE_ADD('month', -1, CURRENT_DATE)) AS VARCHAR) + AND month = CAST(MONTH(DATE_ADD('month', -1, CURRENT_DATE)) AS VARCHAR) + AND line_item_blended_cost > 0 + GROUP BY year, month +`; + +export const serviceBreakdown = ` + SELECT + line_item_product_code as productCode, + SUM(line_item_blended_cost) as totalCost, + COUNT(DISTINCT line_item_resource_id) as resourceCount, + COUNT(DISTINCT line_item_usage_account_id) as accountCount + FROM ${process.env.ATHENA_CU_TABLE} + WHERE year = CAST(YEAR(DATE_ADD('month', -1, CURRENT_DATE)) AS VARCHAR) + AND month = CAST(MONTH(DATE_ADD('month', -1, CURRENT_DATE)) AS VARCHAR) + AND line_item_blended_cost > 0 + GROUP BY line_item_product_code + ORDER BY totalCost DESC + LIMIT 10 +`; + +export const topAccounts = ` + SELECT + line_item_usage_account_id as accountId, + SUM(line_item_blended_cost) as totalCost, + COUNT(DISTINCT line_item_product_code) as serviceCount + FROM ${process.env.ATHENA_CU_TABLE} + WHERE year = CAST(YEAR(DATE_ADD('month', -1, CURRENT_DATE)) AS VARCHAR) + AND month = CAST(MONTH(DATE_ADD('month', -1, CURRENT_DATE)) AS VARCHAR) + AND line_item_blended_cost > 0 + GROUP BY line_item_usage_account_id + ORDER BY totalCost DESC + LIMIT 10 +`; + +export const dailyTrends = ` + SELECT + line_item_usage_start_date as date, + SUM(line_item_blended_cost) as blendedCost, + SUM(line_item_unblended_cost) as unblendedCost + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_start_date >= date_add('day', -30, current_date) + AND line_item_blended_cost > 0 + GROUP BY line_item_usage_start_date + ORDER BY date DESC + LIMIT 31 +`; + +export const todaySpending = ` + SELECT + SUM(line_item_blended_cost) as todaysCost, + COUNT(DISTINCT line_item_product_code) as servicesUsed + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_start_date = CURRENT_DATE + AND line_item_blended_cost > 0`; + +// This week vs last week +export const weeklyComparison = ` + SELECT + CASE + WHEN line_item_usage_start_date >= date_add('day', -7, current_date) THEN 'this_week' + WHEN line_item_usage_start_date >= date_add('day', -14, current_date) THEN 'last_week' + END as week_period, + SUM(line_item_blended_cost) as totalCost + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_start_date >= date_add('day', -14, current_date) + GROUP BY CASE + WHEN line_item_usage_start_date >= date_add('day', -7, current_date) THEN 'this_week' + WHEN line_item_usage_start_date >= date_add('day', -14, current_date) THEN 'last_week' + END +`; + +// Service drill-down queries +export const serviceAccountBreakdown = ` + SELECT + line_item_usage_account_id as accountId, + SUM(line_item_blended_cost) as totalCost, + COUNT(DISTINCT line_item_resource_id) as resourceCount, + COUNT(DISTINCT line_item_usage_start_date) as daysActive + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_product_code = '%serviceCode%' + AND line_item_usage_start_date >= date_add('day', -%days%, current_date) + AND line_item_blended_cost > 0 + GROUP BY line_item_usage_account_id + ORDER BY totalCost DESC +`; + +export const serviceTrends = ` + SELECT + line_item_usage_start_date as date, + SUM(line_item_blended_cost) as dailyCost, + COUNT(DISTINCT line_item_usage_account_id) as accountCount + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_product_code = '%serviceCode%' + AND line_item_usage_start_date >= date_add('day', -%days%, current_date) + AND line_item_blended_cost > 0 + GROUP BY line_item_usage_start_date + ORDER BY date DESC +`; + +// Account drill-down queries +export const accountServiceBreakdown = ` + SELECT + line_item_product_code as productCode, + SUM(line_item_blended_cost) as totalCost, + COUNT(DISTINCT line_item_resource_id) as resourceCount, + COUNT(DISTINCT line_item_usage_start_date) as daysActive + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_account_id = '%accountId%' + AND line_item_usage_start_date >= date_add('day', -%days%, current_date) + AND line_item_blended_cost > 0 + GROUP BY line_item_product_code + ORDER BY totalCost DESC +`; + +export const accountTrends = ` + SELECT + line_item_usage_start_date as date, + SUM(line_item_blended_cost) as dailyCost, + COUNT(DISTINCT line_item_product_code) as serviceCount + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_account_id = '%accountId%' + AND line_item_usage_start_date >= date_add('day', -%days%, current_date) + AND line_item_blended_cost > 0 + GROUP BY line_item_usage_start_date + ORDER BY date DESC +`; + +// Flexible date range queries +export const trendsWithDateRange = (startDate, endDate) => ` + SELECT + line_item_usage_start_date as date, + SUM(line_item_blended_cost) as blendedCost, + SUM(line_item_unblended_cost) as unblendedCost, + COUNT(DISTINCT line_item_product_code) as serviceCount + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_start_date >= date('${startDate}') + AND line_item_usage_start_date <= date('${endDate}') + GROUP BY line_item_usage_start_date + ORDER BY date DESC +`; + +export const servicesWithDateRange = (startDate, endDate) => ` + SELECT + line_item_product_code as productCode, + SUM(line_item_blended_cost) as totalCost, + COUNT(DISTINCT line_item_usage_account_id) as accountCount, + COUNT(DISTINCT line_item_resource_id) as resourceCount + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_start_date >= date('${startDate}') + AND line_item_usage_start_date <= date('${endDate}') + GROUP BY line_item_product_code + ORDER BY totalCost DESC +`; + +export const accountsWithDateRange = (startDate, endDate) => ` + SELECT + line_item_usage_account_id as accountId, + SUM(line_item_blended_cost) as totalCost, + COUNT(DISTINCT line_item_product_code) as serviceCount, + COUNT(DISTINCT line_item_resource_id) as resourceCount + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_start_date >= date('${startDate}') + AND line_item_usage_start_date <= date('${endDate}') + GROUP BY line_item_usage_account_id + ORDER BY totalCost DESC +`; \ No newline at end of file diff --git a/queries.js b/queries/queries.js similarity index 100% rename from queries.js rename to queries/queries.js diff --git a/server.js b/server.js index 22c0191..50fb95f 100644 --- a/server.js +++ b/server.js @@ -1,11 +1,16 @@ import Fastify from "fastify"; import sequelizePlugin from "./plugins/sequelize.js"; import dotenv from "dotenv"; +import cors from "@fastify/cors"; import { executeQueryAsync, retrieveResultsAsync } from "./services/athena.js"; dotenv.config(); -import * as queries from "./queries.js"; +import * as queries from "./queries/queries.js"; +import * as dashboardQueries from "./queries/dashboard.js"; const server = Fastify({ logger: true }); +await server.register(cors, { + origin: "*", +}); server.register(sequelizePlugin); server.get("/", async (request, reply) => { @@ -220,6 +225,109 @@ server.get("/invoices/:invoiceId/accounts/:accountId/products/:productCode/usage return results; }); +server.get("/dashboard/summary", async (request, reply) => { + const [currentQueryId, previousQueryId] = await Promise.all([ + executeQueryAsync(dashboardQueries.currentMonthSummary), + executeQueryAsync(dashboardQueries.previousMonthSummary) + ]); + const [currentResults, previousResults] = await Promise.all([ + retrieveResultsAsync(currentQueryId), + retrieveResultsAsync(previousQueryId) + ]); + return { + current: currentResults[0] || {}, + previous: previousResults[0] || {} + }; +}); + +server.get("/dashboard/services", async (request, reply) => { + const queryExecutionId = await executeQueryAsync(dashboardQueries.serviceBreakdown); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/dashboard/trends", async (request, reply) => { + const queryExecutionId = await executeQueryAsync(dashboardQueries.dailyTrends); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/dashboard/accounts", async (request, reply) => { + const queryExecutionId = await executeQueryAsync(dashboardQueries.topAccounts); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/dashboard/today", async (request, reply) => { + const queryExecutionId = await executeQueryAsync(dashboardQueries.todaySpending); + const results = await retrieveResultsAsync(queryExecutionId); + return results[0] || {}; +}); + +server.get("/dashboard/weekly", async (request, reply) => { + const queryExecutionId = await executeQueryAsync(dashboardQueries.weeklyComparison); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/dashboard/services/:serviceCode/accounts", async (request, reply) => { + const { days = 30 } = request.query; + const query = dashboardQueries.serviceAccountBreakdown + .replace('%serviceCode%', request.params.serviceCode) + .replace('%days%', days); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return { + serviceCode: request.params.serviceCode, + accounts: results, + totalCost: results.reduce((sum, account) => sum + parseFloat(account.totalCost || 0), 0).toFixed(2) + }; +}); + +server.get("/dashboard/services/:serviceCode/trends", async (request, reply) => { + const { days = 30 } = request.query; + const query = dashboardQueries.serviceTrends + .replace('%serviceCode%', request.params.serviceCode) + .replace('%days%', days); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return { + serviceCode: request.params.serviceCode, + trends: results, + totalCost: results.reduce((sum, day) => sum + parseFloat(day.dailyCost || 0), 0).toFixed(2) + }; +}); + +// Account drill-down routes +server.get("/dashboard/accounts/:accountId/services", async (request, reply) => { + const { days = 30 } = request.query; + const query = dashboardQueries.accountServiceBreakdown + .replace('%accountId%', request.params.accountId) + .replace('%days%', days); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return { + accountId: request.params.accountId, + services: results, + totalCost: results.reduce((sum, service) => sum + parseFloat(service.totalCost || 0), 0).toFixed(2) + }; +}); + +server.get("/dashboard/accounts/:accountId/trends", async (request, reply) => { + const { days = 30 } = request.query; + const query = dashboardQueries.accountTrends + .replace('%accountId%', request.params.accountId) + .replace('%days%', days); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return { + accountId: request.params.accountId, + trends: results, + totalCost: results.reduce((sum, day) => sum + parseFloat(day.dailyCost || 0), 0).toFixed(2) + }; +}); + + try { await server.listen({ port: 3000 }) } catch (err) {