Skip to content

Commit 00ec188

Browse files
committed
Now returning the cached status and cachedAt timestamp
1 parent 8399985 commit 00ec188

File tree

9 files changed

+110
-26
lines changed

9 files changed

+110
-26
lines changed

src/index.test.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@ describe('WeatherPlus Library', () => {
113113
const weatherPlus = new WeatherPlus();
114114
const response = await weatherPlus.getWeather(lat, lng);
115115
const expectedResponse: IWeatherData = {
116-
provider: 'nws',
117116
dewPoint: {
118117
value: 20,
119118
unit: 'C',
@@ -130,6 +129,8 @@ describe('WeatherPlus Library', () => {
130129
value: 'Sunny',
131130
unit: 'string',
132131
},
132+
provider: 'nws',
133+
cached: false,
133134
};
134135
expect(response).toEqual(expectedResponse);
135136
});
@@ -278,8 +279,15 @@ describe('WeatherPlus Library', () => {
278279
const response1 = await weatherPlus.getWeather(lat, lng);
279280
const response2 = await weatherPlus.getWeather(lat, lng);
280281

281-
expect(response1).toEqual(response2);
282-
// The second call should use cached data
282+
// The second call should use cached data but otherwise be the same
283+
expect(response1.cached).toBe(false);
284+
expect(response2.cached).toBe(true);
285+
expect(response1.cachedAt).toBeUndefined();
286+
expect(response2.cachedAt).toBeDefined();
287+
expect(response1.dewPoint).toEqual(response2.dewPoint);
288+
expect(response1.humidity).toEqual(response2.humidity);
289+
expect(response1.temperature).toEqual(response2.temperature);
290+
expect(response1.conditions).toEqual(response2.conditions);
283291
});
284292

285293
it('should export InvalidProviderLocationError', () => {

src/interfaces.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ export enum IWeatherKey {
1212
conditions = 'conditions',
1313
}
1414

15-
export interface IWeatherData {
15+
export interface IWeatherProviderWeatherData {
1616
[IWeatherKey.dewPoint]: IDewPoint;
1717
[IWeatherKey.humidity]: IRelativeHumidity;
1818
[IWeatherKey.temperature]: ITemperature;
1919
[IWeatherKey.conditions]: IConditions;
20+
}
21+
22+
export interface IWeatherData extends IWeatherProviderWeatherData {
2023
provider: string;
24+
cached: boolean;
25+
cachedAt?: string; // ISO-8601 formatted date string
2126
}
2227

2328
export type IBaseWeatherProperty<T, U extends IWeatherUnits> = {

src/providers/IWeatherProvider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { IWeatherData } from '../interfaces';
1+
import { IWeatherProviderWeatherData } from '../interfaces';
22

33
export interface IWeatherProvider {
44
name: string;
5-
getWeather(lat: number, lng: number): Promise<IWeatherData>;
5+
getWeather(lat: number, lng: number): Promise<IWeatherProviderWeatherData>;
66
}

src/providers/nws/client.test.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,6 @@ describe('NWSProvider', () => {
6565
humidity: { value: 80, unit: 'percent' },
6666
temperature: { value: 20, unit: 'C' },
6767
conditions: { value: 'Clear', unit: 'string' },
68-
provider: 'nws',
6968
});
7069
});
7170

@@ -133,7 +132,6 @@ describe('NWSProvider', () => {
133132
humidity: { value: 80, unit: 'percent' },
134133
temperature: { value: 20, unit: 'C' },
135134
conditions: { value: 'Clear', unit: 'string' },
136-
provider: 'nws',
137135
});
138136
});
139137
});

src/providers/nws/client.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios from 'axios';
22
import debug from 'debug';
3-
import { IWeatherData, IWeatherKey, IWeatherUnits } from '../../interfaces';
3+
import { IWeatherData, IWeatherKey, IWeatherProviderWeatherData, IWeatherUnits } from '../../interfaces';
44
import {
55
IGridpointsStations,
66
IPointsLatLngResponse,
@@ -18,7 +18,7 @@ export const WEATHER_KEYS = Object.values(IWeatherKey);
1818
export class NWSProvider implements IWeatherProvider {
1919
name = 'nws';
2020

21-
public async getWeather(lat: number, lng: number): Promise<IWeatherData> {
21+
public async getWeather(lat: number, lng: number): Promise<IWeatherProviderWeatherData> {
2222
// Check if the location is within the US
2323
if (!isLocationInUS(lat, lng)) {
2424
throw new InvalidProviderLocationError(
@@ -27,7 +27,7 @@ export class NWSProvider implements IWeatherProvider {
2727
}
2828

2929
const data: Partial<IWeatherData> = {};
30-
const weatherData: IWeatherData[] = [];
30+
const weatherData: IWeatherProviderWeatherData[] = [];
3131

3232
try {
3333
const observationStations = await fetchObservationStationUrl(lat, lng);
@@ -75,7 +75,6 @@ export class NWSProvider implements IWeatherProvider {
7575
if (Object.keys(data).length === 0) {
7676
throw new Error('Invalid observation data');
7777
}
78-
data.provider = 'nws';
7978

8079
return data as IWeatherData;
8180
} catch (error) {
@@ -150,10 +149,9 @@ async function fetchLatestObservation(
150149
}
151150
}
152151

153-
function convertToWeatherData(observation: any): IWeatherData {
152+
function convertToWeatherData(observation: any): IWeatherProviderWeatherData {
154153
const properties = observation.properties;
155154
return {
156-
provider: 'nws',
157155
dewPoint: {
158156
value: properties.dewpoint.value,
159157
unit:

src/providers/openweather/client.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ describe('OpenWeatherProvider', () => {
5050
humidity: { value: 80, unit: 'percent' },
5151
temperature: { value: 20, unit: 'C' },
5252
conditions: { value: 'clear sky', unit: 'string' },
53-
provider: 'openweather',
5453
});
5554
});
5655

src/providers/openweather/client.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios from 'axios';
22
import debug from 'debug';
3-
import { IWeatherData, IWeatherKey, IWeatherUnits } from '../../interfaces';
3+
import { IWeatherUnits, IWeatherProviderWeatherData } from '../../interfaces';
44
import { IOpenWeatherResponse } from './interfaces';
55
import { IWeatherProvider } from '../IWeatherProvider';
66

@@ -17,7 +17,7 @@ export class OpenWeatherProvider implements IWeatherProvider {
1717
this.apiKey = apiKey;
1818
}
1919

20-
public async getWeather(lat: number, lng: number): Promise<IWeatherData> {
20+
public async getWeather(lat: number, lng: number): Promise<IWeatherProviderWeatherData> {
2121
const url = `https://api.openweathermap.org/data/3.0/onecall`;
2222

2323
const params = {
@@ -39,9 +39,8 @@ export class OpenWeatherProvider implements IWeatherProvider {
3939
}
4040
}
4141

42-
function convertToWeatherData(data: IOpenWeatherResponse): IWeatherData {
42+
function convertToWeatherData(data: IOpenWeatherResponse): IWeatherProviderWeatherData {
4343
return {
44-
provider: 'openweather',
4544
dewPoint: {
4645
value: data.current.dew_point,
4746
unit: IWeatherUnits.C,

src/weatherService.test.ts

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,15 @@ describe('WeatherService', () => {
9090
temperature: { value: 15, unit: IWeatherUnits.C },
9191
conditions: { value: 'Sunny', unit: IWeatherUnits.string },
9292
provider: 'nws',
93+
cached: false,
94+
// cachedAt is undefined when data is freshly fetched
9395
};
9496

9597
const weather = await weatherService.getWeather(lat, lng);
9698

9799
expect(weather).toEqual(expectedWeatherData);
100+
expect(weather.cached).toBe(false);
101+
expect(weather.cachedAt).toBeUndefined();
98102
});
99103

100104
it('should fallback to the next provider if the first provider does not support the location', async () => {
@@ -114,6 +118,7 @@ describe('WeatherService', () => {
114118
temperature: { value: 18, unit: IWeatherUnits.C },
115119
conditions: { value: 'Cloudy', unit: IWeatherUnits.string },
116120
provider: 'openweather',
121+
cached: false,
117122
};
118123

119124
const weather = await weatherService.getWeather(lat, lng);
@@ -144,12 +149,14 @@ describe('WeatherService', () => {
144149
});
145150

146151
it('should use cached weather data if available', async () => {
147-
const cachedWeatherData = {
152+
const cachedWeatherData: IWeatherData = {
148153
dewPoint: { value: 11, unit: IWeatherUnits.C },
149154
humidity: { value: 75, unit: IWeatherUnits.percent },
150155
temperature: { value: 16, unit: IWeatherUnits.C },
151156
conditions: { value: 'Overcast', unit: IWeatherUnits.string },
152157
provider: 'nws',
158+
cached: true,
159+
cachedAt: '2023-10-15T12:00:00Z',
153160
};
154161

155162
const cacheGetMock = jest
@@ -167,6 +174,8 @@ describe('WeatherService', () => {
167174
const weather = await weatherService.getWeather(lat, lng);
168175

169176
expect(weather).toEqual(cachedWeatherData);
177+
expect(weather.cached).toBe(true);
178+
expect(weather.cachedAt).toBe('2023-10-15T12:00:00Z');
170179
expect(cacheGetMock).toHaveBeenCalled();
171180
expect(cacheSetMock).not.toHaveBeenCalled();
172181
});
@@ -252,6 +261,8 @@ describe('WeatherService', () => {
252261
temperature: { value: 18, unit: IWeatherUnits.C },
253262
conditions: { value: 'Partly Cloudy', unit: IWeatherUnits.string },
254263
provider: 'openweather',
264+
cached: false,
265+
// cachedAt is undefined when data is freshly fetched
255266
};
256267

257268
const mockProvider: IWeatherProvider = {
@@ -275,6 +286,8 @@ describe('WeatherService', () => {
275286
expect(mockProvider.getWeather).toHaveBeenCalledWith(latitude, longitude);
276287
expect(result).toEqual(mockWeatherData);
277288
expect(result.provider).toBe('openweather'); // Verify provider name
289+
expect(result.cached).toBe(false);
290+
expect(result.cachedAt).toBeUndefined();
278291
});
279292

280293
it('should bypass cache when bypassCache option is true', async () => {
@@ -283,9 +296,19 @@ describe('WeatherService', () => {
283296
set: jest.fn(),
284297
};
285298

299+
const mockWeatherData: IWeatherData = {
300+
dewPoint: { value: 15, unit: IWeatherUnits.C },
301+
humidity: { value: 60, unit: IWeatherUnits.percent },
302+
temperature: { value: 25, unit: IWeatherUnits.C },
303+
conditions: { value: 'Clear', unit: IWeatherUnits.string },
304+
provider: 'mockProvider',
305+
cached: false,
306+
// cachedAt is undefined when data is freshly fetched
307+
};
308+
286309
const mockProvider: IWeatherProvider = {
287310
name: 'mockProvider',
288-
getWeather: jest.fn().mockResolvedValue({ temperature: 25 }),
311+
getWeather: jest.fn().mockResolvedValue(mockWeatherData),
289312
};
290313

291314
const weatherService = new WeatherService({
@@ -309,10 +332,58 @@ describe('WeatherService', () => {
309332
// Expect provider.getWeather to be called
310333
expect(mockProvider.getWeather).toHaveBeenCalledWith(latitude, longitude);
311334

312-
// Expect cache.set to be called with new data
313-
expect(mockCache.set).toHaveBeenCalledWith(expect.any(String), JSON.stringify(result));
335+
// Expect cache.set to be called with new data including cached: true and cachedAt
336+
expect(mockCache.set).toHaveBeenCalled();
337+
338+
// Capture the arguments passed to cache.set
339+
const [cacheKey, cacheValue] = mockCache.set.mock.calls[0];
340+
341+
// Verify that the cache key is a string
342+
expect(cacheKey).toEqual(expect.any(String));
343+
344+
// Parse the cached value
345+
const cachedData = JSON.parse(cacheValue);
346+
347+
// Verify the cached data
348+
expect(cachedData).toEqual({
349+
...mockWeatherData,
350+
cached: true,
351+
cachedAt: expect.any(String),
352+
});
314353

315354
// Verify the result
316-
expect(result).toEqual({ temperature: 25 });
355+
expect(result).toEqual(mockWeatherData);
356+
expect(result.cached).toBe(false);
357+
expect(result.cachedAt).toBeUndefined();
358+
});
359+
360+
// Add a test to verify cachedAt when data is fetched from cache
361+
it('should have valid cachedAt when data is retrieved from cache', async () => {
362+
const cachedWeatherData: IWeatherData = {
363+
dewPoint: { value: 11, unit: IWeatherUnits.C },
364+
humidity: { value: 75, unit: IWeatherUnits.percent },
365+
temperature: { value: 16, unit: IWeatherUnits.C },
366+
conditions: { value: 'Overcast', unit: IWeatherUnits.string },
367+
provider: 'nws',
368+
cached: true,
369+
cachedAt: '2023-10-15T12:00:00Z',
370+
};
371+
372+
const cacheGetMock = jest
373+
.fn()
374+
.mockResolvedValue(JSON.stringify(cachedWeatherData));
375+
376+
// Replace cache methods with mocks
377+
(weatherService as any).cache.get = cacheGetMock;
378+
379+
const lat = 38.8977;
380+
const lng = -77.0365;
381+
382+
const weather = await weatherService.getWeather(lat, lng);
383+
384+
expect(weather).toEqual(cachedWeatherData);
385+
expect(weather.cached).toBe(true);
386+
expect(weather.cachedAt).toBe('2023-10-15T12:00:00Z');
387+
expect(cacheGetMock).toHaveBeenCalled();
317388
});
318389
});

src/weatherService.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { IWeatherProvider } from './providers/IWeatherProvider';
77
import { ProviderFactory } from './providers/providerFactory';
88
import { InvalidProviderLocationError } from './errors';
99
import { isLocationInUS } from './utils/locationUtils';
10+
import { IWeatherData } from './interfaces';
1011

1112
const log = debug('weather-plus');
1213

@@ -117,10 +118,15 @@ export class WeatherService {
117118
}
118119

119120
// Attempt to get weather data from the provider
120-
const weather = await provider.getWeather(geohashLat, geohashLng);
121+
const weather: Partial<IWeatherData> = await provider.getWeather(geohashLat, geohashLng);
122+
weather.provider = provider.name;
121123

124+
// Add cached and cachedAt property to the weather data
125+
const weatherForCache = { ...weather, cached: true, cachedAt: new Date().toISOString() };
122126
// Store the retrieved weather data in cache
123-
await this.cache.set(locationGeohash, JSON.stringify(weather));
127+
await this.cache.set(locationGeohash, JSON.stringify(weatherForCache));
128+
// In this case, we are setting cached to false because we just retrieved fresh data from the provider.
129+
weather.cached = false;
124130

125131
// Return the weather data
126132
return weather;

0 commit comments

Comments
 (0)