From 692398e77bf1cb622b44612ec08f3ace5f1a2228 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:02:51 +0800 Subject: [PATCH 01/17] initial setup --- src/data.js | 22 +++++++++++++++++ src/data.test.js | 32 +++++++++++++++++++++++++ src/data/external/electricityMapsApi.js | 15 ++++++++++++ src/index.js | 11 +++++++-- 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 src/data.js create mode 100644 src/data.test.js create mode 100644 src/data/external/electricityMapsApi.js diff --git a/src/data.js b/src/data.js new file mode 100644 index 0000000..a22f85d --- /dev/null +++ b/src/data.js @@ -0,0 +1,22 @@ +"use strict"; +import ElectricityMapsApi from "./data/external/electricityMapsApi.js"; + +class DataSources { + constructor() { + this.source = undefined; + this.get = undefined; + } + + set(source) { + switch (source) { + case "electricityMapsApi": + this.source = new ElectricityMapsApi(); + this.get = this.source.get; + break; + default: + throw new Error(`Unknown data source: ${source}`); + } + } +} +export { DataSources }; +export default DataSources; diff --git a/src/data.test.js b/src/data.test.js new file mode 100644 index 0000000..12232b3 --- /dev/null +++ b/src/data.test.js @@ -0,0 +1,32 @@ +"use strict"; + +import DataSources from "./data.js"; + +describe("DataSources", () => { + let dataSources; + describe("set", () => { + beforeEach(() => { + dataSources = new DataSources(); + }); + it("throws an error when the data source is unknown", () => { + expect(() => dataSources.set("unknown")).toThrow( + new Error("Unknown data source: unknown") + ); + }); + it("sets the source correctly", () => { + expect(() => dataSources.set("electricityMapsApi")).not.toThrow( + new Error("Unknown data source: unknown") + ); + }); + }); + describe("get", () => { + beforeEach(() => { + dataSources = new DataSources(); + dataSources.set("electricityMapsApi"); + }); + it("returns the correct data", async () => { + const data = await dataSources.get("FR"); + expect(data.countryCode).toBe("FR"); + }); + }); +}); diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js new file mode 100644 index 0000000..9db00e3 --- /dev/null +++ b/src/data/external/electricityMapsApi.js @@ -0,0 +1,15 @@ +class ElectricityMapsApi { + constructor() { + this.baseUrl = "https://api.electricitymap.org/v3"; + } + + async get(countryCode) { + // const url = `${this.baseUrl}/country/latest/${countryCode}`; + // const response = await fetch(url); + // const data = await response.json(); + return { countryCode }; + } +} + +export { ElectricityMapsApi }; +export default ElectricityMapsApi; diff --git a/src/index.js b/src/index.js index 628f4e6..d2a5fd3 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,14 @@ import co2 from "./co2.js"; import hosting from "./hosting.js"; +import dataSources from "./data.js"; import averageIntensity from "./data/average-intensities.min.js"; import marginalIntensity from "./data/marginal-intensities-2021.min.js"; -export { co2, hosting, averageIntensity, marginalIntensity }; -export default { co2, hosting, averageIntensity, marginalIntensity }; +export { co2, hosting, averageIntensity, marginalIntensity, dataSources }; +export default { + co2, + hosting, + averageIntensity, + marginalIntensity, + dataSources, +}; From 688b8bf5199be40cf6d4c5f7d92f192fcba7ef20 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:07:20 +0800 Subject: [PATCH 02/17] allow data source to provide own functions --- src/data.js | 2 -- src/data.test.js | 6 +++++- src/data/external/electricityMapsApi.js | 7 +++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/data.js b/src/data.js index a22f85d..c43d689 100644 --- a/src/data.js +++ b/src/data.js @@ -4,14 +4,12 @@ import ElectricityMapsApi from "./data/external/electricityMapsApi.js"; class DataSources { constructor() { this.source = undefined; - this.get = undefined; } set(source) { switch (source) { case "electricityMapsApi": this.source = new ElectricityMapsApi(); - this.get = this.source.get; break; default: throw new Error(`Unknown data source: ${source}`); diff --git a/src/data.test.js b/src/data.test.js index 12232b3..93c34cc 100644 --- a/src/data.test.js +++ b/src/data.test.js @@ -25,8 +25,12 @@ describe("DataSources", () => { dataSources.set("electricityMapsApi"); }); it("returns the correct data", async () => { - const data = await dataSources.get("FR"); + const data = await dataSources.source.get("FR"); expect(data.countryCode).toBe("FR"); }); + it("returns the correct zones", async () => { + const zones = await dataSources.source.getZones(); + expect(zones).toEqual(["a", "b", "c"]); + }); }); }); diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index 9db00e3..fbcd036 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -9,6 +9,13 @@ class ElectricityMapsApi { // const data = await response.json(); return { countryCode }; } + + async getZones() { + // const url = `${this.baseUrl}/zones`; + // const response = await fetch(url); + // const data = await response.json(); + return ["a", "b", "c"]; + } } export { ElectricityMapsApi }; From 74648b68dbfcfe495580b6756cc076ef3b9c3b8d Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:11:50 +0800 Subject: [PATCH 03/17] add tests for electricity maps API --- src/data/external/electricityMapsApi.test.js | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/data/external/electricityMapsApi.test.js diff --git a/src/data/external/electricityMapsApi.test.js b/src/data/external/electricityMapsApi.test.js new file mode 100644 index 0000000..8c4f6e9 --- /dev/null +++ b/src/data/external/electricityMapsApi.test.js @@ -0,0 +1,22 @@ +"use strict"; + +import ElectricityMapApi from "./electricityMapApi.js"; + +describe("ElectricityMapApi", () => { + let electricityMapApi; + beforeEach(() => { + electricityMapApi = new ElectricityMapApi(); + }); + describe("get", () => { + it("returns the correct data", async () => { + const data = await electricityMapApi.get("FR"); + expect(data.countryCode).toBe("FR"); + }); + }); + describe("getZones", () => { + it("returns the correct zones", async () => { + const zones = await electricityMapApi.getZones(); + expect(zones).toEqual(["a", "b", "c"]); + }); + }); +}); From daba0691dda4b9bde3bad75e3dd6bb349a2d80a0 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:12:02 +0800 Subject: [PATCH 04/17] update tests for data sources --- src/data.test.js | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/data.test.js b/src/data.test.js index 93c34cc..17e9692 100644 --- a/src/data.test.js +++ b/src/data.test.js @@ -4,12 +4,12 @@ import DataSources from "./data.js"; describe("DataSources", () => { let dataSources; - describe("set", () => { + describe("sets the source", () => { beforeEach(() => { dataSources = new DataSources(); }); it("throws an error when the data source is unknown", () => { - expect(() => dataSources.set("unknown")).toThrow( + expect(() => dataSources.set()).toThrow( new Error("Unknown data source: unknown") ); }); @@ -19,18 +19,4 @@ describe("DataSources", () => { ); }); }); - describe("get", () => { - beforeEach(() => { - dataSources = new DataSources(); - dataSources.set("electricityMapsApi"); - }); - it("returns the correct data", async () => { - const data = await dataSources.source.get("FR"); - expect(data.countryCode).toBe("FR"); - }); - it("returns the correct zones", async () => { - const zones = await dataSources.source.getZones(); - expect(zones).toEqual(["a", "b", "c"]); - }); - }); }); From c2c063de180806f462bbaf0552607d49e3481690 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Thu, 25 Jan 2024 22:12:33 +0800 Subject: [PATCH 05/17] initial implementation for current, historic, and zones endpoints --- src/data/external/electricityMapsApi.js | 47 +++++++++++++++++++------ 1 file changed, 36 insertions(+), 11 deletions(-) diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index fbcd036..057d69c 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -1,20 +1,45 @@ class ElectricityMapsApi { - constructor() { - this.baseUrl = "https://api.electricitymap.org/v3"; + constructor(options) { + this.baseUrl = "https://api-access.electricitymaps.com/free-tier/"; + this.apiKey = options.apiKey; } - async get(countryCode) { - // const url = `${this.baseUrl}/country/latest/${countryCode}`; - // const response = await fetch(url); - // const data = await response.json(); - return { countryCode }; + async get(zone, lat, lon) { + const url = `${this.baseUrl}/carbon-intensity/latest?lat=${lat}&lon=${lon}&zone=${zone}`; + const response = await fetch(url); + const data = await response.json(); + return { data }; + } + + async getHistory(zone, lat, lon, dataTime = undefined) { + const url = `${this.baseUrl}/carbon-intensity/history?lat=${lat}&lon=${lon}&zone=${zone}`; + const response = await fetch(url); + const data = await response.json(); + + if (data.status === "error") { + throw new Error(data.message); + } + + if (dataTime) { + try { + return data.history.filter((d) => d.datetime === dataTime)[0]; + } catch { + throw new Error( + `No data for ${dataTime} in ${ + (zone | ("lat: " + lat), "lon: " + lon) + }` + ); + } + } + + return data.history; } async getZones() { - // const url = `${this.baseUrl}/zones`; - // const response = await fetch(url); - // const data = await response.json(); - return ["a", "b", "c"]; + const url = `${this.baseUrl}/zones`; + const response = await fetch(url); + const data = await response.json(); + return data; } } From 36a63a8b255969267d24d268365f49b5719c3ac7 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sat, 27 Jan 2024 21:00:44 +0800 Subject: [PATCH 06/17] rename function --- src/data/external/electricityMapsApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index 057d69c..f0f1cd1 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -4,7 +4,7 @@ class ElectricityMapsApi { this.apiKey = options.apiKey; } - async get(zone, lat, lon) { + async getLatest(zone, lat, lon) { const url = `${this.baseUrl}/carbon-intensity/latest?lat=${lat}&lon=${lon}&zone=${zone}`; const response = await fetch(url); const data = await response.json(); From f3f94fe2b6cc92719fb4d92a7412538ae50987a5 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:14:17 +0800 Subject: [PATCH 07/17] update error wording --- src/data.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data.test.js b/src/data.test.js index 17e9692..687e809 100644 --- a/src/data.test.js +++ b/src/data.test.js @@ -8,9 +8,9 @@ describe("DataSources", () => { beforeEach(() => { dataSources = new DataSources(); }); - it("throws an error when the data source is unknown", () => { + it("throws an error when the data source is not defined", () => { expect(() => dataSources.set()).toThrow( - new Error("Unknown data source: unknown") + new Error("Unknown data source: undefined") ); }); it("sets the source correctly", () => { From 6b440f7f6de012a98c78918b0e1faa0525940c20 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:14:26 +0800 Subject: [PATCH 08/17] add more tests --- src/data/external/electricityMapsApi.test.js | 176 ++++++++++++++++++- 1 file changed, 169 insertions(+), 7 deletions(-) diff --git a/src/data/external/electricityMapsApi.test.js b/src/data/external/electricityMapsApi.test.js index 8c4f6e9..1374de3 100644 --- a/src/data/external/electricityMapsApi.test.js +++ b/src/data/external/electricityMapsApi.test.js @@ -1,22 +1,184 @@ "use strict"; -import ElectricityMapApi from "./electricityMapApi.js"; +import ElectricityMapApi from "./electricityMapsApi.js"; + +global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({}), + }) +); describe("ElectricityMapApi", () => { let electricityMapApi; + let electricityMapApiWithoutAuthToken; beforeEach(() => { - electricityMapApi = new ElectricityMapApi(); + electricityMapApi = new ElectricityMapApi({ + authToken: "test-1234", + }); + electricityMapApiWithoutAuthToken = new ElectricityMapApi(); + fetch.mockClear(); }); - describe("get", () => { + describe("it sets the Auth token", () => { + it("should set the Auth token correctly", () => { + electricityMapApi = new ElectricityMapApi({ + authToken: "test-1234", + }); + expect(electricityMapApi.authToken).toBe("test-1234"); + }); + }); + describe("get latest grid intensity", () => { + it("needs an Auth token", async () => { + await expect( + electricityMapApiWithoutAuthToken.getLatest("TWN", 0, 0) + ).rejects.toThrow( + new Error( + "An authentication token is required to access this endpoint." + ) + ); + }); + it("returns an error when the response is an error", async () => { + fetch.mockImplementation(() => + Promise.resolve({ + json: () => + Promise.resolve({ + status: "error", + message: "Zone 'TWN' does not exist.", + }), + }) + ); + await expect(electricityMapApi.getLatest("TWN", 0, 0)).rejects.toThrow( + new Error("Zone 'TWN' does not exist.") + ); + }); it("returns the correct data", async () => { - const data = await electricityMapApi.get("FR"); - expect(data.countryCode).toBe("FR"); + fetch.mockImplementation(() => + Promise.resolve({ + json: () => + Promise.resolve({ + zone: "TW", + carbonIntensity: 492, + datetime: "2024-01-27T13:00:00.000Z", + updatedAt: "2024-01-27T12:47:05.424Z", + createdAt: "2024-01-24T13:49:22.390Z", + emissionFactorType: "lifecycle", + isEstimated: true, + estimationMethod: "TIME_SLICER_AVERAGE", + }), + }) + ); + const data = await electricityMapApi.getLatest("TW", 0, 0); + expect(data.data).toEqual({ + zone: "TW", + carbonIntensity: 492, + datetime: "2024-01-27T13:00:00.000Z", + updatedAt: "2024-01-27T12:47:05.424Z", + createdAt: "2024-01-24T13:49:22.390Z", + emissionFactorType: "lifecycle", + isEstimated: true, + estimationMethod: "TIME_SLICER_AVERAGE", + }); + }); + }); + describe("get historical grid intensity", () => { + it("needs an Auth token", async () => { + await expect( + electricityMapApiWithoutAuthToken.getHistory("TWN", 0, 0) + ).rejects.toThrow( + new Error( + "An authentication token is required to access this endpoint." + ) + ); + }); + it("returns an error when the response is an error", async () => { + fetch.mockImplementation(() => + Promise.resolve({ + json: () => + Promise.resolve({ + status: "error", + message: "Zone 'TWN' does not exist.", + }), + }) + ); + electricityMapApi.authToken = "test-1234"; + await expect(electricityMapApi.getHistory("TWN", 0, 0)).rejects.toThrow( + new Error("Zone 'TWN' does not exist.") + ); }); }); - describe("getZones", () => { + describe("get zones", () => { it("returns the correct zones", async () => { + fetch.mockImplementation(() => + Promise.resolve({ + json: () => + Promise.resolve({ + AU: { + zoneName: "Australia", + access: [ + "carbon-intensity/latest", + "carbon-intensity/history", + "power-breakdown/latest", + "power-breakdown/history", + "home-assistant/latest", + "home-assistant/past", + "home-assistant/past-range", + "home-assistant/history", + "home-assistant/forecast", + "/updated-since", + ], + }, + "AU-NSW": { + countryName: "Australia", + zoneName: "New South Wales", + access: [ + "carbon-intensity/latest", + "carbon-intensity/history", + "power-breakdown/latest", + "power-breakdown/history", + "home-assistant/latest", + "home-assistant/past", + "home-assistant/past-range", + "home-assistant/history", + "home-assistant/forecast", + "/updated-since", + ], + }, + }), + }) + ); const zones = await electricityMapApi.getZones(); - expect(zones).toEqual(["a", "b", "c"]); + expect(zones).toEqual({ + AU: { + zoneName: "Australia", + access: [ + "carbon-intensity/latest", + "carbon-intensity/history", + "power-breakdown/latest", + "power-breakdown/history", + "home-assistant/latest", + "home-assistant/past", + "home-assistant/past-range", + "home-assistant/history", + "home-assistant/forecast", + "/updated-since", + ], + }, + "AU-NSW": { + countryName: "Australia", + zoneName: "New South Wales", + access: [ + "carbon-intensity/latest", + "carbon-intensity/history", + "power-breakdown/latest", + "power-breakdown/history", + "home-assistant/latest", + "home-assistant/past", + "home-assistant/past-range", + "home-assistant/history", + "home-assistant/forecast", + "/updated-since", + ], + }, + }); }); }); }); From e76e2b86acfc793fd10d9b21030013251e8615db Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:14:50 +0800 Subject: [PATCH 09/17] update request URL --- src/data/external/electricityMapsApi.js | 44 +++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index f0f1cd1..b550638 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -1,19 +1,51 @@ class ElectricityMapsApi { constructor(options) { - this.baseUrl = "https://api-access.electricitymaps.com/free-tier/"; - this.apiKey = options.apiKey; + this.baseUrl = "https://api-access.electricitymaps.com/free-tier"; + this.authToken = options?.authToken || undefined; } async getLatest(zone, lat, lon) { - const url = `${this.baseUrl}/carbon-intensity/latest?lat=${lat}&lon=${lon}&zone=${zone}`; - const response = await fetch(url); + if (!this.authToken || this.authToken === undefined) { + throw new Error( + "An authentication token is required to access this endpoint." + ); + } + + const query = `${lat ? `lat=${lat}&` : ""}${lon ? `lon=${lon}&` : ""}${ + zone ? `zone=${zone}` : "" + }`; + const url = `${this.baseUrl}/carbon-intensity/latest?${query}`; + const response = await fetch(url, { + method: "GET", + headers: { + "auth-token": this.authToken, + }, + }); const data = await response.json(); + + if (data.status === "error") { + throw new Error(data.message); + } + return { data }; } async getHistory(zone, lat, lon, dataTime = undefined) { - const url = `${this.baseUrl}/carbon-intensity/history?lat=${lat}&lon=${lon}&zone=${zone}`; - const response = await fetch(url); + if (!this.authToken || this.authToken === undefined) { + throw new Error( + "An authentication token is required to access this endpoint." + ); + } + const query = `${lat ? `lat=${lat}&` : ""}${lon ? `lon=${lon}&` : ""}${ + zone ? `zone=${zone}` : "" + }`; + const url = `${this.baseUrl}/carbon-intensity/history?${query}`; + const response = await fetch(url, { + method: "GET", + headers: { + "auth-token": this.authToken, + }, + }); const data = await response.json(); if (data.status === "error") { From 5d8f86e30da0c50235c6def9e6a4bf9d28892730 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:15:22 +0800 Subject: [PATCH 10/17] remove datetime filter function --- src/data/external/electricityMapsApi.js | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index b550638..47f3e4a 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -52,18 +52,6 @@ class ElectricityMapsApi { throw new Error(data.message); } - if (dataTime) { - try { - return data.history.filter((d) => d.datetime === dataTime)[0]; - } catch { - throw new Error( - `No data for ${dataTime} in ${ - (zone | ("lat: " + lat), "lon: " + lon) - }` - ); - } - } - return data.history; } From fadc78957b426f48905813cde5518317a4ab194e Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sun, 28 Jan 2024 20:18:44 +0800 Subject: [PATCH 11/17] Updated from main --- .all-contributorsrc | 11 +++++- .esbuild.browser.js | 3 ++ .esbuild.common.js | 7 ++++ .esbuild.esm.js | 4 ++- .esbuild.node.js | 2 ++ __mocks__/https.js | 28 +++++++++++++++ package.json | 5 +-- src/helpers/index.js | 12 ++++++- src/hosting-api.js | 24 +++++++++---- src/hosting-api.test.js | 80 +++++++++++++++++++++++++++++------------ src/hosting-node.js | 57 +++++++++++++++++------------ src/hosting.js | 5 +-- src/hosting.test.js | 25 ++++++++++--- 13 files changed, 201 insertions(+), 62 deletions(-) create mode 100644 .esbuild.common.js create mode 100644 __mocks__/https.js diff --git a/.all-contributorsrc b/.all-contributorsrc index 07a31f1..2069729 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -132,8 +132,17 @@ "bug", "code" ] + }, + { + "login": "sfishel18", + "name": "Simon Fishel", + "avatar_url": "https://avatars.githubusercontent.com/u/294695?v=4", + "profile": "https://github.com/sfishel18", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, "linkToUsage": false -} +} \ No newline at end of file diff --git a/.esbuild.browser.js b/.esbuild.browser.js index 8fee5a6..d50dd28 100644 --- a/.esbuild.browser.js +++ b/.esbuild.browser.js @@ -1,4 +1,7 @@ +const esbuildCommon = require("./.esbuild.common"); + require('esbuild').buildSync({ + ...esbuildCommon, entryPoints: ['src/index.js'], outdir: 'dist/iife', globalName: 'co2', diff --git a/.esbuild.common.js b/.esbuild.common.js new file mode 100644 index 0000000..b93abed --- /dev/null +++ b/.esbuild.common.js @@ -0,0 +1,7 @@ +const CO2JS_VERSION = require("./package.json").version; + +module.exports = { + define: { + "process.env.CO2JS_VERSION": JSON.stringify(CO2JS_VERSION), + }, +}; diff --git a/.esbuild.esm.js b/.esbuild.esm.js index a8e4066..327f527 100644 --- a/.esbuild.esm.js +++ b/.esbuild.esm.js @@ -3,7 +3,8 @@ const esbuild = require('esbuild') // For this build however we need to filter out some extra files // that are used for nodejs, but not in browsers, so we use the // library directly instead of using `esbuild-plugin-glob` as a plugin -const glob = require('tiny-glob'); +const glob = require('tiny-glob') +const esbuildCommon = require('./.esbuild.common') async function main() { const results = await glob('src/**/!(*.test.js|test-constants.js|!(*.js))') @@ -12,6 +13,7 @@ async function main() { const justBrowserCompatibleFiles = results.filter(filepath => !filepath.endsWith('node.js')) esbuild.build({ + ...esbuildCommon, entryPoints: justBrowserCompatibleFiles, bundle: false, minify: false, diff --git a/.esbuild.node.js b/.esbuild.node.js index 8254d8e..6f5991b 100644 --- a/.esbuild.node.js +++ b/.esbuild.node.js @@ -1,7 +1,9 @@ const { globPlugin } = require('esbuild-plugin-glob'); +const esbuildCommon = require('./.esbuild.common'); function main() { require('esbuild').build({ + ...esbuildCommon, entryPoints: ['src/**/!(*.test.js|test-constants.js|!(*.js))'], bundle: false, minify: false, diff --git a/__mocks__/https.js b/__mocks__/https.js new file mode 100644 index 0000000..a4cee94 --- /dev/null +++ b/__mocks__/https.js @@ -0,0 +1,28 @@ +import { getApiRequestHeaders } from "../src/helpers/index.js"; +const https = jest.createMockFromModule("https"); +import { Stream } from "stream"; + +const stream = new Stream(); + +https.get.mockImplementation((url, options, callback) => { + url, { headers: getApiRequestHeaders("TestRunner") }, callback(stream); + if (url.includes("greencheckmulti")) { + stream.emit( + "data", + Buffer.from( + `{"google.com": {"url":"google.com","hosted_by":"Google Inc.","hosted_by_website":"https://www.google.com","partner":null,"green":true}}` + ) + ); + } else { + stream.emit( + "data", + Buffer.from( + `{"url":"google.com","hosted_by":"Google Inc.","hosted_by_website":"https://www.google.com","partner":null,"green":true}` + ) + ); + } + + stream.emit("end"); +}); + +module.exports = https; diff --git a/package.json b/package.json index 71503e6..51abf65 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ "intensity-data:average": "node data/functions/generate_average_co2.js", "intensity-data:marginal": "node data/functions/generate_marginal_co2.js", "intensity-data": "npm run intensity-data:average && npm run intensity-data:marginal && npm run format-data", - "format-data": "cd data && prettier --write '**/*.{js,json}'" + "format-data": "cd data && prettier --write '**/*.{js,json}'", + "version": "npm run build" }, "keywords": [ "sustainability", @@ -75,4 +76,4 @@ "type": "git", "url": "https://github.com/thegreenwebfoundation/co2.js.git" } -} +} \ No newline at end of file diff --git a/src/helpers/index.js b/src/helpers/index.js index 9faf7ac..29db234 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -172,4 +172,14 @@ function parseOptions(options) { return adjustments; } -export { formatNumber, parseOptions }; +/** + * Returns an object containing all the HTTP headers to use when making a request to the Green Web Foundation API. + * @param {string} comment - Optional. The app, site, or organisation that is making the request. + * + * @returns {import('http').OutgoingHttpHeaders} + */ +function getApiRequestHeaders(comment = "") { + return { "User-Agent": `co2js/${process.env.CO2JS_VERSION} ${comment}` }; +} + +export { formatNumber, parseOptions, getApiRequestHeaders }; diff --git a/src/hosting-api.js b/src/hosting-api.js index fc75235..60337a7 100644 --- a/src/hosting-api.js +++ b/src/hosting-api.js @@ -1,27 +1,34 @@ "use strict"; +import { getApiRequestHeaders } from "./helpers/index.js"; + /** * Check if a string or array of domains has been provided * @param {string|array} domain - The domain to check, or an array of domains to be checked. + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. */ -function check(domain) { +function check(domain, userAgentIdentifier) { // is it a single domain or an array of them? if (typeof domain === "string") { - return checkAgainstAPI(domain); + return checkAgainstAPI(domain, userAgentIdentifier); } else { - return checkDomainsAgainstAPI(domain); + return checkDomainsAgainstAPI(domain, userAgentIdentifier); } } /** * Check if a domain is hosted by a green web host by querying the Green Web Foundation API. * @param {string} domain - The domain to check. + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. * @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host. */ -async function checkAgainstAPI(domain) { +async function checkAgainstAPI(domain, userAgentIdentifier) { const req = await fetch( - `https://api.thegreenwebfoundation.org/greencheck/${domain}` + `https://api.thegreenwebfoundation.org/greencheck/${domain}`, + { + headers: getApiRequestHeaders(userAgentIdentifier), + } ); const res = await req.json(); return res.green; @@ -30,15 +37,18 @@ async function checkAgainstAPI(domain) { /** * Check if an array of domains is hosted by a green web host by querying the Green Web Foundation API. * @param {array} domains - An array of domains to check. + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. * @returns {array} - An array of domains that are hosted by a green web host. */ -async function checkDomainsAgainstAPI(domains) { +async function checkDomainsAgainstAPI(domains, userAgentIdentifier) { try { const apiPath = "https://api.thegreenwebfoundation.org/v2/greencheckmulti"; const domainsString = JSON.stringify(domains); - const req = await fetch(`${apiPath}/${domainsString}`); + const req = await fetch(`${apiPath}/${domainsString}`, { + headers: getApiRequestHeaders(userAgentIdentifier), + }); const allGreenCheckResults = await req.json(); diff --git a/src/hosting-api.test.js b/src/hosting-api.test.js index 8e5646a..fc37294 100644 --- a/src/hosting-api.test.js +++ b/src/hosting-api.test.js @@ -1,36 +1,72 @@ "use strict"; -import hosting from "./hosting-node.js"; -import nock from "nock"; +import hosting from "./hosting-api.js"; /* eslint-disable jest/no-disabled-tests */ + +process.env.CO2JS_VERSION = "1.2.34"; +const requestHeaderComment = "TestRunner"; + +global.fetch = jest.fn(() => + Promise.resolve({ + json: () => Promise.resolve({ green: true }), + }) +); + describe("hostingAPI", () => { + beforeEach(() => { + fetch.mockClear(); + }); describe("checking a single domain with #check", () => { - it.skip("using the API", async () => { - const scope = nock("https://api.thegreenwebfoundation.org/") - .get("/greencheck/google.com") - .reply(200, { - url: "google.com", - green: true, - }); + it("using the API", async () => { const res = await hosting.check("google.com"); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { "User-Agent": "co2js/1.2.34 " }, + }) + ); + expect(res).toEqual(true); + }); + it("sets the correct user agent header", async () => { + const res = await hosting.check("google.com", requestHeaderComment); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { "User-Agent": "co2js/1.2.34 TestRunner" }, + }) + ); expect(res).toEqual(true); }); }); describe("implicitly checking multiple domains with #check", () => { - it.skip("using the API", async () => { - const scope = nock("https://api.thegreenwebfoundation.org/") - .get("/v2/greencheckmulti/[%22google.com%22,%22kochindustries.com%22]") - .reply(200, { - "google.com": { - url: "google.com", - green: true, - }, - "kochindustries.com": { - url: "kochindustries.com", - green: null, - }, - }); + it("using the API", async () => { + fetch.mockImplementation(() => + Promise.resolve({ + json: () => + Promise.resolve({ + "google.com": { url: "google.com", green: true }, + }), + }) + ); const res = await hosting.check(["google.com", "kochindustries.com"]); + expect(fetch).toHaveBeenCalledTimes(1); + expect(res).toContain("google.com"); + }); + it("sets the correct user agent header", async () => { + const res = await hosting.check( + ["google.com", "kochindustries.com"], + requestHeaderComment + ); + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenLastCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { "User-Agent": "co2js/1.2.34 TestRunner" }, + }) + ); expect(res).toContain("google.com"); }); }); diff --git a/src/hosting-node.js b/src/hosting-node.js index b1ecda8..87eadfb 100644 --- a/src/hosting-node.js +++ b/src/hosting-node.js @@ -11,33 +11,39 @@ This lets us keep the total library small, and dependencies minimal. import https from "https"; import hostingJSON from "./hosting-json.node.js"; +import { getApiRequestHeaders } from "./helpers/index.js"; /** * Accept a url and perform an http request, returning the body * for parsing as JSON. * * @param {string} url + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. * @return {string} */ -async function getBody(url) { +async function getBody(url, userAgentIdentifier) { return new Promise(function (resolve, reject) { // Do async job - const req = https.get(url, function (res) { - if (res.statusCode < 200 || res.statusCode >= 300) { - return reject( - new Error( - `Could not get info from: ${url}. Status Code: ${res.statusCode}` - ) - ); - } - const data = []; + const req = https.get( + url, + { headers: getApiRequestHeaders(userAgentIdentifier) }, + function (res) { + if (res.statusCode < 200 || res.statusCode >= 300) { + return reject( + new Error( + `Could not get info from: ${url}. Status Code: ${res.statusCode}` + ) + ); + } + const data = []; - res.on("data", (chunk) => { - data.push(chunk); - }); + res.on("data", (chunk) => { + data.push(chunk); + }); - res.on("end", () => resolve(Buffer.concat(data).toString())); - }); + res.on("end", () => resolve(Buffer.concat(data).toString())); + } + ); req.end(); }); } @@ -46,30 +52,35 @@ async function getBody(url) { * Check if a domain is hosted by a green web host. * @param {string|array} domain - The domain to check, or an array of domains to be checked. * @param {object} db - Optional. A database object to use for lookups. + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. * @returns {boolean|array} - A boolean if a string was provided, or an array of booleans if an array of domains was provided. */ -function check(domain, db) { +function check(domain, db, userAgentIdentifier) { if (db) { return hostingJSON.check(domain, db); } // is it a single domain or an array of them? if (typeof domain === "string") { - return checkAgainstAPI(domain); + return checkAgainstAPI(domain, userAgentIdentifier); } else { - return checkDomainsAgainstAPI(domain); + return checkDomainsAgainstAPI(domain, userAgentIdentifier); } } /** * Check if a domain is hosted by a green web host by querying the Green Web Foundation API. * @param {string} domain - The domain to check. + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. * @returns {boolean} - A boolean indicating whether the domain is hosted by a green web host. */ -async function checkAgainstAPI(domain) { +async function checkAgainstAPI(domain, userAgentIdentifier) { const res = JSON.parse( - await getBody(`https://api.thegreenwebfoundation.org/greencheck/${domain}`) + await getBody( + `https://api.thegreenwebfoundation.org/greencheck/${domain}`, + userAgentIdentifier + ) ); return res.green; } @@ -77,15 +88,17 @@ async function checkAgainstAPI(domain) { /** * Check if an array of domains is hosted by a green web host by querying the Green Web Foundation API. * @param {array} domains - An array of domains to check. + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. * @returns {array} - An array of domains that are hosted by a green web host. */ -async function checkDomainsAgainstAPI(domains) { +async function checkDomainsAgainstAPI(domains, userAgentIdentifier) { try { const allGreenCheckResults = JSON.parse( await getBody( `https://api.thegreenwebfoundation.org/v2/greencheckmulti/${JSON.stringify( domains - )}` + )}`, + userAgentIdentifier ) ); return hostingJSON.greenDomainsFromResults(allGreenCheckResults); diff --git a/src/hosting.js b/src/hosting.js index e9974f1..d0020eb 100644 --- a/src/hosting.js +++ b/src/hosting.js @@ -5,10 +5,11 @@ import hostingAPI from "./hosting-api.js"; /** * Check if a domain is hosted by a green web host. * @param {string|array} domain - The domain to check, or an array of domains to be checked. + * @param {string} userAgentIdentifier - Optional. The app, site, or organisation that is making the request. * @returns {boolean|array} - A boolean if a string was provided, or an array of booleans if an array of domains was provided. */ -function check(domain) { - return hostingAPI.check(domain); +function check(domain, userAgentIdentifier) { + return hostingAPI.check(domain, userAgentIdentifier); } export default { diff --git a/src/hosting.test.js b/src/hosting.test.js index a280120..666eb83 100644 --- a/src/hosting.test.js +++ b/src/hosting.test.js @@ -1,12 +1,18 @@ "use strict"; import fs from "fs"; +import https from "https"; import path from "path"; import pagexray from "pagexray"; import hosting from "./hosting-node.js"; +jest.mock("https"); + +process.env.CO2JS_VERSION = "1.2.34"; +const requestHeaderComment = "TestRunner"; + const jsonPath = path.resolve( __dirname, "..", @@ -17,6 +23,7 @@ const jsonPath = path.resolve( describe("hosting", () => { let har; + let httpsGetSpy; beforeEach(() => { har = JSON.parse( fs.readFileSync( @@ -24,6 +31,8 @@ describe("hosting", () => { "utf8" ) ); + httpsGetSpy = jest.spyOn(https, "get"); + jest.clearAllMocks(); }); describe("checking all domains on a page object with #checkPage", () => { it("returns a list of green domains, when passed a page object", async () => { @@ -53,16 +62,24 @@ describe("hosting", () => { }); describe("checking a single domain with #check", () => { it("use the API instead", async () => { - const db = await hosting.loadJSON(jsonPath); const res = await hosting.check("google.com"); expect(res).toEqual(true); }); + it("sets the correct user agent header", async () => { + await hosting.check("google.com", null, requestHeaderComment); + expect(httpsGetSpy).toHaveBeenCalledTimes(1); + expect(httpsGetSpy).toHaveBeenLastCalledWith( + expect.any(String), + expect.objectContaining({ + headers: { "User-Agent": "co2js/1.2.34 TestRunner" }, + }), + expect.any(Function) + ); + }); }); describe("checking multiple domains with #check", () => { it("Use the API", async () => { - const db = await hosting.loadJSON(jsonPath); - - const res = await hosting.check(["google.com", "kochindustries.com"]); + const res = await hosting.check(["google.com", "pchome.com"]); expect(res).toContain("google.com"); }); }); From 9c8bcf2e69d48314a4a02385b5fd1eed1b409eb5 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:29:47 +0800 Subject: [PATCH 12/17] add more tests --- __mocks__/electricityMapsApi.js | 25 ++++ src/data/external/electricityMapsApi.test.js | 125 ++++++++++++++----- 2 files changed, 122 insertions(+), 28 deletions(-) create mode 100644 __mocks__/electricityMapsApi.js diff --git a/__mocks__/electricityMapsApi.js b/__mocks__/electricityMapsApi.js new file mode 100644 index 0000000..def1930 --- /dev/null +++ b/__mocks__/electricityMapsApi.js @@ -0,0 +1,25 @@ +export const taiwanHistoricalData = { + history: [ + { + zone: "TW", + carbonIntensity: 492, + datetime: "2024-01-27T13:00:00.000Z", + updatedAt: "2024-01-27T12:47:05.424Z", + createdAt: "2024-01-24T13:49:22.390Z", + emissionFactorType: "lifecycle", + isEstimated: true, + estimationMethod: "TIME_SLICER_AVERAGE", + }, + ], +}; + +export const taiwanLatestData = { + zone: "TW", + carbonIntensity: 492, + datetime: "2024-01-27T13:00:00.000Z", + updatedAt: "2024-01-27T12:47:05.424Z", + createdAt: "2024-01-24T13:49:22.390Z", + emissionFactorType: "lifecycle", + isEstimated: true, + estimationMethod: "TIME_SLICER_AVERAGE", +}; diff --git a/src/data/external/electricityMapsApi.test.js b/src/data/external/electricityMapsApi.test.js index 1374de3..b3d56b9 100644 --- a/src/data/external/electricityMapsApi.test.js +++ b/src/data/external/electricityMapsApi.test.js @@ -1,6 +1,10 @@ "use strict"; import ElectricityMapApi from "./electricityMapsApi.js"; +import { + taiwanHistoricalData, + taiwanLatestData, +} from "../../../__mocks__/electricityMapsApi.js"; global.fetch = jest.fn(() => Promise.resolve({ @@ -36,8 +40,31 @@ describe("ElectricityMapApi", () => { ) ); }); + it("returns the correct data", async () => { + fetch.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(taiwanLatestData), + }) + ); + const data = await electricityMapApi.getLatest("TW", 0, 0); + expect(data.data).toEqual(taiwanLatestData); + + fetch.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(taiwanLatestData), + }) + ); + + const data2 = await electricityMapApi.getLatest( + undefined, + "23.6978", + "120.9605" + ); + + expect(data2.data).toEqual(taiwanLatestData); + }); it("returns an error when the response is an error", async () => { - fetch.mockImplementation(() => + fetch.mockImplementationOnce(() => Promise.resolve({ json: () => Promise.resolve({ @@ -50,33 +77,28 @@ describe("ElectricityMapApi", () => { new Error("Zone 'TWN' does not exist.") ); }); - it("returns the correct data", async () => { - fetch.mockImplementation(() => - Promise.resolve({ - json: () => - Promise.resolve({ - zone: "TW", - carbonIntensity: 492, - datetime: "2024-01-27T13:00:00.000Z", - updatedAt: "2024-01-27T12:47:05.424Z", - createdAt: "2024-01-24T13:49:22.390Z", - emissionFactorType: "lifecycle", - isEstimated: true, - estimationMethod: "TIME_SLICER_AVERAGE", - }), - }) + it("requires a zone or lat & lon value", async () => { + await expect(electricityMapApi.getLatest()).rejects.toThrow( + new Error( + "Either a zone or a latitude and longitude value is required." + ) + ); + + await expect( + electricityMapApi.getLatest("TW", undefined, undefined) + ).resolves.not.toThrow( + new Error( + "Either a zone or a latitude and longitude value is required." + ) + ); + + await expect( + electricityMapApi.getLatest(undefined, "0", "0") + ).resolves.not.toThrow( + new Error( + "Either a zone or a latitude and longitude value is required." + ) ); - const data = await electricityMapApi.getLatest("TW", 0, 0); - expect(data.data).toEqual({ - zone: "TW", - carbonIntensity: 492, - datetime: "2024-01-27T13:00:00.000Z", - updatedAt: "2024-01-27T12:47:05.424Z", - createdAt: "2024-01-24T13:49:22.390Z", - emissionFactorType: "lifecycle", - isEstimated: true, - estimationMethod: "TIME_SLICER_AVERAGE", - }); }); }); describe("get historical grid intensity", () => { @@ -89,8 +111,32 @@ describe("ElectricityMapApi", () => { ) ); }); + it("returns the correct data", async () => { + fetch.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(taiwanHistoricalData), + }) + ); + const data = await electricityMapApi.getHistory("TW", 0, 0); + expect(data).toEqual(taiwanHistoricalData.history); + + fetch.mockImplementationOnce(() => + Promise.resolve({ + json: () => Promise.resolve(taiwanHistoricalData), + }) + ); + + const data2 = await electricityMapApi.getHistory( + undefined, + "23.6978", + "120.9605" + ); + + expect(data2).toEqual(taiwanHistoricalData.history); + }); + it("returns an error when the response is an error", async () => { - fetch.mockImplementation(() => + fetch.mockImplementationOnce(() => Promise.resolve({ json: () => Promise.resolve({ @@ -104,6 +150,29 @@ describe("ElectricityMapApi", () => { new Error("Zone 'TWN' does not exist.") ); }); + it("requires a zone or lat & lon value", async () => { + await expect(electricityMapApi.getHistory()).rejects.toThrow( + new Error( + "Either a zone or a latitude and longitude value is required." + ) + ); + + await expect( + electricityMapApi.getHistory("TW", undefined, undefined) + ).resolves.not.toThrow( + new Error( + "Either a zone or a latitude and longitude value is required." + ) + ); + + await expect( + electricityMapApi.getHistory(undefined, "0", "0") + ).resolves.not.toThrow( + new Error( + "Either a zone or a latitude and longitude value is required." + ) + ); + }); }); describe("get zones", () => { it("returns the correct zones", async () => { From 95b7f3d67954aea70eb2dd63ac7c17a305ca1f18 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:30:05 +0800 Subject: [PATCH 13/17] check for a zone or lat & lon values --- src/data/external/electricityMapsApi.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index 47f3e4a..e1fd8cd 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -11,6 +11,12 @@ class ElectricityMapsApi { ); } + if (!zone && (!lat || !lon)) { + throw new Error( + "Either a zone or a latitude and longitude value is required." + ); + } + const query = `${lat ? `lat=${lat}&` : ""}${lon ? `lon=${lon}&` : ""}${ zone ? `zone=${zone}` : "" }`; @@ -30,15 +36,24 @@ class ElectricityMapsApi { return { data }; } - async getHistory(zone, lat, lon, dataTime = undefined) { + async getHistory(zone, lat, lon) { if (!this.authToken || this.authToken === undefined) { throw new Error( "An authentication token is required to access this endpoint." ); } + + if (!zone && (!lat || !lon)) { + console.log(zone, lat, lon); + throw new Error( + "Either a zone or a latitude and longitude value is required." + ); + } + const query = `${lat ? `lat=${lat}&` : ""}${lon ? `lon=${lon}&` : ""}${ zone ? `zone=${zone}` : "" }`; + const url = `${this.baseUrl}/carbon-intensity/history?${query}`; const response = await fetch(url, { method: "GET", From ee05f6d811e67cee0e342a46e91cf4747e902863 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:31:18 +0800 Subject: [PATCH 14/17] remove console log --- src/data/external/electricityMapsApi.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index e1fd8cd..b651d15 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -44,7 +44,6 @@ class ElectricityMapsApi { } if (!zone && (!lat || !lon)) { - console.log(zone, lat, lon); throw new Error( "Either a zone or a latitude and longitude value is required." ); From 75b9ce1bbf8346294356e5a4c1aea86e2f6fb417 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:36:33 +0800 Subject: [PATCH 15/17] remove mocks file --- __mocks__/electricityMapsApi.js | 25 ---------------- src/data/external/electricityMapsApi.test.js | 30 +++++++++++++++++--- 2 files changed, 26 insertions(+), 29 deletions(-) delete mode 100644 __mocks__/electricityMapsApi.js diff --git a/__mocks__/electricityMapsApi.js b/__mocks__/electricityMapsApi.js deleted file mode 100644 index def1930..0000000 --- a/__mocks__/electricityMapsApi.js +++ /dev/null @@ -1,25 +0,0 @@ -export const taiwanHistoricalData = { - history: [ - { - zone: "TW", - carbonIntensity: 492, - datetime: "2024-01-27T13:00:00.000Z", - updatedAt: "2024-01-27T12:47:05.424Z", - createdAt: "2024-01-24T13:49:22.390Z", - emissionFactorType: "lifecycle", - isEstimated: true, - estimationMethod: "TIME_SLICER_AVERAGE", - }, - ], -}; - -export const taiwanLatestData = { - zone: "TW", - carbonIntensity: 492, - datetime: "2024-01-27T13:00:00.000Z", - updatedAt: "2024-01-27T12:47:05.424Z", - createdAt: "2024-01-24T13:49:22.390Z", - emissionFactorType: "lifecycle", - isEstimated: true, - estimationMethod: "TIME_SLICER_AVERAGE", -}; diff --git a/src/data/external/electricityMapsApi.test.js b/src/data/external/electricityMapsApi.test.js index b3d56b9..94826a1 100644 --- a/src/data/external/electricityMapsApi.test.js +++ b/src/data/external/electricityMapsApi.test.js @@ -1,10 +1,32 @@ "use strict"; import ElectricityMapApi from "./electricityMapsApi.js"; -import { - taiwanHistoricalData, - taiwanLatestData, -} from "../../../__mocks__/electricityMapsApi.js"; + +const taiwanHistoricalData = { + history: [ + { + zone: "TW", + carbonIntensity: 492, + datetime: "2024-01-27T13:00:00.000Z", + updatedAt: "2024-01-27T12:47:05.424Z", + createdAt: "2024-01-24T13:49:22.390Z", + emissionFactorType: "lifecycle", + isEstimated: true, + estimationMethod: "TIME_SLICER_AVERAGE", + }, + ], +}; + +const taiwanLatestData = { + zone: "TW", + carbonIntensity: 492, + datetime: "2024-01-27T13:00:00.000Z", + updatedAt: "2024-01-27T12:47:05.424Z", + createdAt: "2024-01-24T13:49:22.390Z", + emissionFactorType: "lifecycle", + isEstimated: true, + estimationMethod: "TIME_SLICER_AVERAGE", +}; global.fetch = jest.fn(() => Promise.resolve({ From f5f5cc83c794c334eb1e64cbed598e05844d28d5 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Sun, 28 Jan 2024 21:52:29 +0800 Subject: [PATCH 16/17] add name and docs --- src/data/external/electricityMapsApi.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index b651d15..1aabf4f 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -2,6 +2,8 @@ class ElectricityMapsApi { constructor(options) { this.baseUrl = "https://api-access.electricitymaps.com/free-tier"; this.authToken = options?.authToken || undefined; + this.name = "Electricity Maps API - Free Tier"; + this.docs = "https://static.electricitymaps.com/api/docs/index.html"; } async getLatest(zone, lat, lon) { From 8b97e47a6aeb12a490973be3008c1a70a1fcbb14 Mon Sep 17 00:00:00 2001 From: fershad <27988517+fershad@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:58:06 +0800 Subject: [PATCH 17/17] add jsdoc comments --- src/data.js | 8 +++ src/data/external/electricityMapsApi.js | 80 +++++++++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/data.js b/src/data.js index c43d689..002d157 100644 --- a/src/data.js +++ b/src/data.js @@ -3,9 +3,17 @@ import ElectricityMapsApi from "./data/external/electricityMapsApi.js"; class DataSources { constructor() { + /** + * @type {String} - The source of the data. + */ this.source = undefined; } + /** + * Set the source of the data. + * @param {string} source - The source of the data. + * @throws {Error} Will throw an error if the source is unknown or not provided. + */ set(source) { switch (source) { case "electricityMapsApi": diff --git a/src/data/external/electricityMapsApi.js b/src/data/external/electricityMapsApi.js index 1aabf4f..2907d40 100644 --- a/src/data/external/electricityMapsApi.js +++ b/src/data/external/electricityMapsApi.js @@ -1,11 +1,77 @@ +/** + * Type definition for the options of the ElectricityMapsApi. + * @typedef {Object} ElectricityMapsApiOptions + * @property {string} authToken - The authentication token for the API. + */ + +/** + * @typedef {Object} LatestData + * @property {string} zone - The zone identifier. + * @property {number} carbonIntensity - The carbon intensity value. + * @property {string} datetime - The date and time of the data. + * @property {string} updatedAt - The date and time the data was last updated. + * @property {string} createdAt - The date and time the data was created. + * @property {string} emissionFactorType - The type of emission factor used. + * @property {boolean} isEstimated - Whether the data is estimated. + * @property {string} estimationMethod - The method used to estimate the data. + */ + +/** + * @typedef {Object} ZoneData + * @property {string} countryName - The name of the country the zone belongs to. + * @property {string} zoneName - The zone identifier. + * @property {string[]} access - an array of strings listing the API endpoints the zone can be accessed from + */ + +/** + * @typedef {Object} HistoryData + * @property {Object[]} history - An array of historical data. + * @property {string} history.zone - The zone identifier. + * @property {number} history.carbonIntensity - The carbon intensity value. + * @property {string} history.datetime - The date and time of the data. + * @property {string} history.updatedAt - The date and time the data was last updated. + * @property {string} history.createdAt - The date and time the data was created. + * @property {string} history.emissionFactorType - The type of emission factor used. + * @property {boolean} history.isEstimated - Whether the data is estimated. + * @property {string} history.estimationMethod - The method used to estimate the data. + */ + class ElectricityMapsApi { + /** + * Create an instance of ElectricityMapsApi. + * @param {ElectricityMapsApiOptions} options - The options for the ElectricityMapsApi. + */ constructor(options) { + /** + * @type {string} The base URL of the API. + */ this.baseUrl = "https://api-access.electricitymaps.com/free-tier"; + + /** + * @type {string} The authentication token for the API. + */ this.authToken = options?.authToken || undefined; + + /** + * @type {string} The name of the API. + */ this.name = "Electricity Maps API - Free Tier"; + + /** + * @type {string} The documentation URL of the API. + */ this.docs = "https://static.electricitymaps.com/api/docs/index.html"; } + /** + * Fetches the latest grid intensity data from the API. + * @param {string} zone - The zone identifier. + * @param {string} lat - The latitude of the location. + * @param {string} lon - The longitude of the location. + * @returns {Promise} A promise that resolves with the latest grid intensity data. + * @throws {Error} Will throw an error if the authentication token is not provided. + * @throws {Error} Will throw an error if the zone or lat & lon are not provided. + */ async getLatest(zone, lat, lon) { if (!this.authToken || this.authToken === undefined) { throw new Error( @@ -38,6 +104,16 @@ class ElectricityMapsApi { return { data }; } + /** + * Fetches the historical grid intensity data from the API. + * @param {string} zone - The zone identifier. + * @param {string} lat - The latitude of the location. + * @param {string} lon - The longitude of the location. + * @returns {Promise} A promise that resolves with the historical grid intensity data. + * @throws {Error} Will throw an error if the authentication token is not provided. + * @throws {Error} Will throw an error if the zone or lat & lon are not provided. + */ + async getHistory(zone, lat, lon) { if (!this.authToken || this.authToken === undefined) { throw new Error( @@ -71,6 +147,10 @@ class ElectricityMapsApi { return data.history; } + /** + * Fetches the zone data from the API. + * @returns {Promise} A promise that resolves with the data for all zones. + */ async getZones() { const url = `${this.baseUrl}/zones`; const response = await fetch(url);