diff --git a/queries.js b/queries.js index 73d5aae..0885bf2 100644 --- a/queries.js +++ b/queries.js @@ -1,8 +1,185 @@ import dotenv from "dotenv"; dotenv.config(); +// /accounts +export const accountsQuery = `SELECT DISTINCT + line_item_usage_account_id AS accountId FROM ${process.env.ATHENA_CU_TABLE};`; + +// /accounts/:accountId/regions +export const regionsQuery = `SELECT DISTINCT + line_item_usage_account_id AS accountId, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode + FROM ${process.env.ATHENA_CU_TABLE} + WHERE line_item_usage_account_id = '%accountId%';`; + +// /accounts/:accountId/regions/:regionCode/products +export const productsByRegionQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + line_item_usage_account_id AS accountId, + line_item_resource_id AS resourceId +FROM ${process.env.ATHENA_CU_TABLE} +WHERE line_item_usage_account_id = '%accountId%' AND LOWER(product_region_code) = LOWER('%regionCode%');`; + +// /accounts/:accountId/regions/:regionCode/products/:productCode +export const productByRegionQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + line_item_usage_account_id AS accountId, + line_item_resource_id AS resourceId +FROM ${process.env.ATHENA_CU_TABLE} +WHERE + line_item_usage_account_id = '%accountId%' AND + product_region_code = '%regionCode%' AND + LOWER(line_item_product_code) = LOWER('%productCode%');`; + +// /accounts/:accountId/regions/:regionCode/products/:productCode/usage +export const productUsageByRegionQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + 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 line_item_usage_account_id = '%accountId%' + AND product_region_code = '%regionCode%' + AND LOWER(line_item_product_code) = LOWER('%productCode%');`; + +// /accounts/:accountId/regions/:regionCode/products/:productCode/usage/:year +export const productUsageByRegionYearQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + 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 line_item_usage_account_id = '%accountId%' + AND product_region_code = '%regionCode%' + AND LOWER(line_item_product_code) = LOWER('%productCode%') + AND year = '%year%';`; + +// /accounts/:accountId/regions/:regionCode/products/:productCode/usage/:year/:month +export const productUsageByRegionYearMonthQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + 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 line_item_usage_account_id = '%accountId%' + AND product_region_code = '%regionCode%' + AND LOWER(line_item_product_code) = LOWER('%productCode%') + AND year = '%year%' + AND month = '%month%';`; + +// /products export const productQuery = `SELECT DISTINCT - line_item_resource_id AS resourceId, line_item_product_code AS productCode, - line_item_usage_account_id AS accountId -FROM ${process.env.ATHENA_CU_TABLE};`; \ No newline at end of file + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + line_item_usage_account_id AS accountId, + line_item_resource_id AS resourceId +FROM ${process.env.ATHENA_CU_TABLE};`; + +export const productByCodeQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + line_item_usage_account_id AS accountId, + line_item_resource_id AS resourceId +FROM ${process.env.ATHENA_CU_TABLE} + WHERE LOWER(line_item_product_code) = LOWER('%productCode%');`; + +export const productByCodeUsageQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + 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 LOWER(line_item_product_code) = LOWER('%productCode%');`; + +export const productByCodeUsageYearQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + 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 LOWER(line_item_product_code) = LOWER('%productCode%') + AND year = '%year%';`; + +export const productByCodeUsageYearMonthQuery = `SELECT DISTINCT + line_item_product_code AS productCode, + COALESCE(NULLIF(TRIM(product_region_code), ''), 'global') AS regionCode, + 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 LOWER(line_item_product_code) = LOWER('%productCode%') + AND year = '%year%' + AND month = '%month%';`; \ No newline at end of file diff --git a/server.js b/server.js index 3172a54..2057852 100644 --- a/server.js +++ b/server.js @@ -15,16 +15,136 @@ server.get("/", async (request, reply) => { return { hello: "world" }; }); +server.get("/accounts", async (request, reply) => { + const queryExecutionId = await executeQueryAsync(queries.accountsQuery); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/accounts/:accountId/regions", async (request, reply) => { + const query = queries.regionsQuery.replace('%accountId%', request.params.accountId); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/accounts/:accountId/regions/:regionCode/products", async (request, reply) => { + let regionCode = request.params.regionCode; + if (!regionCode || regionCode === 'global') { + regionCode = ''; + } + const query = queries.productsByRegionQuery + .replace('%accountId%', request.params.accountId) + .replace('%regionCode%', regionCode); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/accounts/:accountId/regions/:regionCode/products/:productCode", async (request, reply) => { + let regionCode = request.params.regionCode; + if (!regionCode || regionCode === 'global') { + regionCode = ''; + } + const query = queries.productByRegionQuery + .replace('%accountId%', request.params.accountId) + .replace('%regionCode%', regionCode) + .replace('%productCode%', request.params.productCode); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/accounts/:accountId/regions/:regionCode/products/:productCode/usage", async (request, reply) => { + let regionCode = request.params.regionCode; + if (!regionCode || regionCode === 'global') { + regionCode = ''; + } + const query = queries.productUsageByRegionQuery + .replace('%accountId%', request.params.accountId) + .replace('%regionCode%', regionCode) + .replace('%productCode%', request.params.productCode); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/accounts/:accountId/regions/:regionCode/products/:productCode/usage/:year", async (request, reply) => { + let regionCode = request.params.regionCode; + if (!regionCode || regionCode === 'global') { + regionCode = ''; + } + const query = queries.productUsageByRegionYearQuery + .replace('%accountId%', request.params.accountId) + .replace('%regionCode%', regionCode) + .replace('%productCode%', request.params.productCode) + .replace('%year%', request.params.year); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/accounts/:accountId/regions/:regionCode/products/:productCode/usage/:year/:month", async (request, reply) => { + let regionCode = request.params.regionCode; + if (!regionCode || regionCode === 'global') { + regionCode = ''; + } + const query = queries.productUsageByRegionYearMonthQuery + .replace('%accountId%', request.params.accountId) + .replace('%regionCode%', regionCode) + .replace('%productCode%', request.params.productCode) + .replace('%year%', request.params.year) + .replace('%month%', request.params.month); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + server.get("/products", async (request, reply) => { const query = queries.productQuery; const queryExecutionId = await executeQueryAsync(query); const results = await retrieveResultsAsync(queryExecutionId); - return { results }; + return results; +}); + +server.get("/products/:productCode", async (request, reply) => { + const query = queries.productByCodeQuery + .replace('%productCode%', request.params.productCode); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/products/:productCode/usage", async (request, reply) => { + const query = queries.productByCodeUsageQuery + .replace('%productCode%', request.params.productCode); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/products/:productCode/usage/:year", async (request, reply) => { + const query = queries.productByCodeUsageYearQuery + .replace('%productCode%', request.params.productCode) + .replace('%year%', request.params.year); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; +}); + +server.get("/products/:productCode/usage/:year/:month", async (request, reply) => { + const query = queries.productByCodeUsageYearMonthQuery + .replace('%productCode%', request.params.productCode) + .replace('%year%', request.params.year) + .replace('%month%', request.params.month); + const queryExecutionId = await executeQueryAsync(query); + const results = await retrieveResultsAsync(queryExecutionId); + return results; }); try { await server.listen({ port: 3000 }) } catch (err) { - server.log.error(err) - process.exit(1) + server.log.error(err); + process.exit(1); } diff --git a/services/athena.js b/services/athena.js index 9cd88d3..e480859 100644 --- a/services/athena.js +++ b/services/athena.js @@ -45,17 +45,11 @@ export const retrieveResultsAsync = async (queryExecutionId) => { const result = await athenaClient.send(getQueryResultsCommand); if (!result || !result.ResultSet || !result.ResultSet.Rows) { - return { - statusCode: 404, - body: JSON.stringify([]), - }; + throw new Error('No results'); } let rows = result.ResultSet.Rows; if (0 == rows.length || 1 == rows.length) { - return { - statusCode: 400, - body: JSON.stringify([]), - }; + throw new Error('No result data'); } const columnNames = rows[0].Data.map(item => item.VarCharValue); const items = [];