Update to My Table Sorting Web Component
29 May 2024 | 6:00 pm

Just a quick note. Last year, I blogged a demo of a web component that lets you wrap an existing HTML table and progressively add table sorting. I'm rather proud of that demo and was actually planning on doing a quick video about it, but while testing I encountered two small bugs that somehow missed my earlier rigorous testing. (And by rigorous testing I mean a few minutes of clicking around.)

Specifically, the issue is in the "when clicking to sort, notice if we sorted this column before and if so, reverse the sort" area:

sortCol(e,i) {	let sortToggle = 1;	if(this.lastSort === i) {		this.sortAsc = !this.sortAsc;		if(!this.sortDir) sortToggle = -1;	}	this.lastSort = i;		this.data.sort((a,b) => {		if(a[i] < b[i]) return -1 * sortToggle;		if(a[i] > b[i]) return 1 * sortToggle;		return 0;	});		this.renderTable();}

In the function above, i simply refers to the index of the column that is being sorted. My thinking at the time was - the default is ascending, but if you are clicking the same column as last time, reverse it.

There are two bugs here:

  • One, I'm using sortDir which doesn't even exist. I must have renamed it to sortAsc and missed it. That was an easy fix.
  • The second issue was harder to find. I clicked to sort a column a few times, then clicked another column a few times, then came back, and noticed the second click wouldn't properly change the direction. Why? Because I never revered sortAsc to true on a new column.

So the fix looks like this:

sortCol(e,i) {	let sortToggle = 1;	if(this.lastSort === i) {		this.sortAsc = !this.sortAsc;		if(!this.sortAsc) sortToggle = -1;	} else this.sortAsc = true;		this.lastSort = i;		this.data.sort((a,b) => {		if(a[i] < b[i]) return -1 * sortToggle;		if(a[i] > b[i]) return 1 * sortToggle;		return 0;	});		this.renderTable();}

I'm going to edit the older blog post now and correct the samples, but if you just want to see the finished version, here it is:

See the Pen PE Table for Sorting (2) - Edited by Raymond Camden (@cfjedimaster) on CodePen.

Adding Recommendations to my Blog with Algolia
27 May 2024 | 6:00 pm

I've been using Algolia for my site's search functionality for a few years now and it works great, especially once the free tier expanded to cover the size of my content somewhat better. In that time, I've mainly just stuck to basic search functionality and haven't really touched any of the more advanced features. This weekend I took a look at one I've been meaning to play with for some time, Recommendations.

My thinking was, of course, a way to recommend/suggest content related to the current blog post you may be reading. This distinction is important because as I looked at the Recommendations marketing and documentation, the content is heavily focused on product recommendations. I.e., the typical "you are looking at product X, and these 3 items are often purchased with it" type scenario. That makes perfect sense, but I will say that initially, I assumed what I wanted to do wasn't possible, ie, just straight content recommendations. That may be on me for perhaps skimming the docs a bit quickly, but I share this just in case others have the same reaction as well.

Recommendations are covered by the incredibly generous free tier, but oddly, at least in my look at the pricing page, I don't see that specifically called out. (I've sent my contacts at Algolia feedback on this and certainly, it could just be me missing the obvious.) I was told that the free tier includes 10k "requests" including search. Now, my search page barely gets any traffic, I think I'm the one who uses it the most, but my site itself gets quite a bit of traffic and if every page load is making a call for recommendations, that can quickly add up.

I decided to implement recommendations on my blog with:

  • A Netlify serverless function to proxy the calls to Algolia.
  • Netlify Blob's as a simple caching system.

As an FYI, Algolia's client-side JavaScript API absolutely supports recommendations and I initially built that locally, but removed it once I realized I'd probably blow away my free tier usage.

Here's how I built it.

Enabling Recommendations for My Content #

The first step is to actually enable recommendations which can be done in your Algolia dashboard by - clicking "Recommendations". Yeah, I know, obvious. However - this brings you here:

Algolia Recommendations Dashboard

Beneath this and not in the screenshot was a table named, Existing models, which was blank with no way to add to it. From what I could tell, I needed to select one of the options you see above, but had no real clue due to what I mentioned above - the heavy focus on a product use case. Luckily I had help from an Algolian, Juff (sorry buddy, don't know your real name, but thank you) who told me to use the "Alternative recommendations" model.

This leads you to this UI:

Recommendation model setup UI

The first question, data source, was easy enough, I selected the index for my blog. You can skip the events (for a content-based example like I'm doing), and then add the "key object" attributes, which for my content was my content and title attributes.

The final step is to hit that Start training button and then go take a quick break. This takes a little while. I didn't time it exactly but given the size of my content (nearing seven thousand blog posts), it felt like a reasonable amount of time. I want to say it was less than thirty minutes or so.

When done, you get a really nice visualization and even a bit of sample code as well:

Recommendation sample results and sample code

All in all, relatively painless, but I do wish the "content use case" was more obvious.

Implementation #

As I mentioned above, it's relatively straightforward to get recommendations in JavaScript. I had to add a new script tag (I'm using a 'lite' version of the search SDK), and then a bit of code like so:

const algoliarecommend = window['@algolia/recommend'];const recommendClient = algoliarecommend('0FJBPN4K5D', '8f741f50b983176875b65e252402b140');// using this instead of href so it works in devlet url = ('https://www.raymondcamden.com' + window.location.pathname).slice(0,-1);//console.log(url);let recommendationData = await recommendClient.getRelatedProducts([	{	indexName: 'raymondcamden',	objectID: url,	maxRecommendations:5,        	queryParameters: {		attributesToRetrieve:"title,date,url"	}	},]);let recommendations = recommendationData.results[0].hits;

I do a bit of manipulation to get the proper object ID. My Algolia content is identified by the URL with no trailing slash at the end. Once I have that though, I just call the getRelatedProducts method and that's it. The queryParameters bit there is used to reduce the load of data going back and forth, but all in all, it took just a few minutes.

And then I promptly ripped it out. I scaffolded a new Netlify function and wrote the following:

import { getStore } from "@netlify/blobs";let algCredentials = { appId: process.env.ALG_APP_ID, apiKey: process.env.ALG_API_KEY, indexName: 'raymondcamden' };// difference in minutes, one day basicallylet CACHE_MAX = 24 * 60 * 60 * 1000;export default async (req, context) => {  let params = new URL(req.url).searchParams;  if(!params.get('path')) return new Response("No path!");  let path = 'https://www.raymondcamden.com' + params.get('path');    const recommendationStore = getStore('recommendations');  let recos = await recommendationStore.get(path, { type:'json'});  if(recos) {    let diff = (new Date() - new Date(recos.cached)) / (1000 * 60);    //console.log('diff in ms', diff);    if(diff < CACHE_MAX) return Response.json(recos.recommendations);  }  //console.log('Not in cache, or expired');  let body = {     "requests":[        {            "indexName":"raymondcamden",            "model":"related-products",            "objectID":path,            "threshold":40,            "maxRecommendations":5,            "queryParameters":{                "attributesToRetrieve":"title,date,url"            }        }    ]  }  let resp = await fetch(`https://${algCredentials.appId}-dsn.algolia.net/1/indexes/*/recommendations`, {    method:'POST',    headers:{      'X-Algolia-Application-Id': algCredentials.appId,       'X-Algolia-API-Key': algCredentials.apiKey    },    body:JSON.stringify(body)  });  let results = await resp.json();  if(results.status && results.status === 404) return Response.json([]);  //console.log(results);  let recommendations = results.results[0].hits.map(h => {    return {      "date":h.date,      "url":h.url,      "title":h.title    }  });  //console.log(`for ${path} found ${recommendations.length} recommendations`);  await recommendationStore.setJSON(path, { recommendations, cached: new Date() });  return Response.json(recommendations);};export const config = {  path:"/api/get-recommendations"}

From the top, I import what I need and initialize variables and such. The function itself starts off by looking for the URL in a query string variable. If it exists, I check the cache. If it exists in the cache, and most importantly, is less than a day old, I return the cached version.

Otherwise, I hit the Algolia REST API. I do a bit of manipulation on the results to make it simpler (date, url, and title), cache it, and then return it.

For my blog post on dyanmically creating variables in Postman, here's the result:

[    {        "date": "Mon Jul 24 2017 17:33:00 GMT+0000 (Coordinated Universal Time)",        "url": "https://www.raymondcamden.com/2017/07/24/using-postman-with-openwhisk",        "title": "Using Postman with OpenWhisk"    },    {        "date": "Thu Apr 05 2012 10:04:00 GMT+0000 (Coordinated Universal Time)",        "url": "https://www.raymondcamden.com/2012/04/05/Using-jQuery-to-conditionally-load-requests",        "title": "Using jQuery to conditionally load requests"    },    {        "date": "Mon May 18 2020 00:00:00 GMT+0000 (Coordinated Universal Time)",        "url": "https://www.raymondcamden.com/2020/05/18/integrating-netlify-analytics-and-eleventy",        "title": "Integrating Netlify Analytics and Eleventy"    }]

Honestly, only the first one feels really on target, but as I've tested with other entries, in general, I feel like I'm getting decent results.

The last part, and the one that took me the longest, was figuring out how and where to render it. I decided to append a gray box after the "Support" box at the bottom and above the comments:

async function doRecommendations() {  let url = window.location.pathname;  if(url.slice(-1) === '/') url = url.slice(0,-1);  let recommendationReq = await fetch('/api/get-recommendations?path=' + encodeURIComponent(url));  let recommendations = await recommendationReq.json();  console.log(`${recommendations.length} recommendations found`);  if(recommendations.length === 0) return;  let formatter = new Intl.DateTimeFormat('en-us', {    dateStyle:'long'  });  let reco = `		<div class="author-box">			<div class="author-info">				<h3>Related Content</h3>        <ul>  `;  recommendations.forEach(r => {    reco += `      <li><a href="${r.url}">${r.title} (${formatter.format(new Date(r.date))})</a></li>    `;  });  reco += `      </ul>    </div>  </div>`;  document.querySelector('div.author-box').insertAdjacentHTML('afterend',reco);}

This code is only run on blog posts as it wouldn't make sense on other pages.

And that's it. Honestly, I'm rather pleased by it, but can see myself tweaking the UI later. Let me know what you think and leave a comment below.

Edit After publishing this blog post, I noticed my recommendations were always returning an empty array in production. Turns out, in my local dev, I have the trailing slash, and in prod, I don't, so the last version of the client-side JavaScript, as you see above, checks to see if the last character is a slash. I definitely won't ever make this mistake again. Definitely.

Creating Visualizations in Postman
24 May 2024 | 6:00 pm

Earlier this week, I blogged about a cool Postman feature where you could use scripting to take the result of one API call and use it as a variable that is then used by a second call. For APIs that first require you to exchange credentials for an access token, this is a super useful way to make that process easier. Today I'm following up on that with another useful application of scripting - visualizations. Once again, I've got my coworker Ben to thank for showing me this. Let me show you an example.

When working with Firefly Services and the text to image API, you get a nice JSON response back containing information about the results as well as links to your images. Here's an example where I used the prompt, "Cats writing enterprise software demos.":

{    "version": "2.10.3",    "size": {        "width": 2048,        "height": 2048    },    "predictedPhotoSettings": {        "aperture": 6.3,        "shutterSpeed": 0.0005,        "fieldOfView": 50    },    "outputs": [        {            "seed": 1336901054,            "image": {                "id": "6f8c3bdd-0fdd-4818-91e5-aee0a9d86ef3",                "presignedUrl": "https://pre-signed-firefly-prod.s3.amazonaws.com/images/6f8c3bdd-0fdd-4818-91e5-aee0a9d86ef3?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDA3TX66CSNORXF4%2F20240524%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20240524T152441Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=90557979a1bbcadf338bd80ca884ffb1d39e633f702b861668725ceb2af435c4"            }        },        {            "seed": 1576319647,            "image": {                "id": "9a337feb-dc83-4cb4-8e61-dbd4a3f52911",                "presignedUrl": "https://pre-signed-firefly-prod.s3.amazonaws.com/images/9a337feb-dc83-4cb4-8e61-dbd4a3f52911?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDA3TX66CSNORXF4%2F20240524%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20240524T152441Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=a53ddae53b4815f015275f893211fd562b78bbc09663d4bfbdf32e29a2bec007"            }        }    ]}

In Postman, I can ctrl click (or CMD click on Mac) to open those results in my browser. Easy peasy. However, Postman supports a "Visualize" tab where you can craft your own visualizations of the data. Here's an example of a script that does that:

const template = `{{#each response.outputs}}<p><h3>Result {{seed}}</h3><img src="{{image.presignedUrl}}" style="max-width:500px;max-height:500px"></p>{{/each}}`;pm.visualizer.set(template, { response: pm.response.json() });

I've got a string on top for layout, and in case you don't recognize it, that's Handlebars, an HTML templating language that Postman supports. I simply pass the template and my JSON response and... that's it. I get this nice output:

Sample result

Both images are there, but the second one is beneath the fold in the app. You'll notice I'm using a bit of CSS to shrink the images, and obviously if I wanted to, I could have made them smaller. I could have laid them out left to right instead of vertically. I just whipped up something quick and simple. But the nice thing is, I can see the results without having to leave Postman.

Also, make note of this:

Sample result with red arrow pointing to refresh

That little icon there lets you reload the visualization, which means you can make changes to your script and template and test without making another API call.

While this is a great way to render out results from an API that generates images, there are other good uses for the feature as well. The Pirate Weather API is a great free weather API. This call, https://api.pirateweather.net/forecast//30.216,-92.033?exclude=minutely,hourly,currently, returns a weather forecast for my area. (The {{pirateweather}} part is replaced with my API key in Postman.)

The API returns a ton of data, even with me specifically excluding parts. Here's the result for that call:

{    "latitude": 30.216,    "longitude": -92.033,    "timezone": "America/Chicago",    "offset": -5.0,    "elevation": 46,    "daily": {        "summary": "Partly Cloudy",        "icon": "partly-cloudy-day",        "data": [            {                "time": 1716526800,                "icon": "clear-day",                "summary": "Clear",                "sunriseTime": 1716548976,                "sunsetTime": 1716598848,                "moonPhase": 0.53,                "precipIntensity": 0.0,                "precipIntensityMax": 0.0,                "precipIntensityMaxTime": 1716548400,                "precipProbability": 0.0,                "precipAccumulation": 0.0,                "precipType": "none",                "temperatureHigh": 89.11,                "temperatureHighTime": 1716580800,                "temperatureLow": 77.63,                "temperatureLowTime": 1716616800,                "apparentTemperatureHigh": 99.12,                "apparentTemperatureHighTime": 1716577200,                "apparentTemperatureLow": 77.41,                "apparentTemperatureLowTime": 1716544800,                "dewPoint": 75.43,                "humidity": 0.81,                "pressure": 1009.05,                "windSpeed": 8.97,                "windGust": 18.09,                "windGustTime": 1716577200,                "windBearing": 179.17,                "cloudCover": 0.34,                "uvIndex": 8.65,                "uvIndexTime": 1716577200,                "visibility": 9.35,                "temperatureMin": 76.96,                "temperatureMinTime": 1716548400,                "temperatureMax": 89.11,                "temperatureMaxTime": 1716580800,                "apparentTemperatureMin": 76.96,                "apparentTemperatureMinTime": 1716548400,                "apparentTemperatureMax": 99.12,                "apparentTemperatureMaxTime": 1716577200            },            {                "time": 1716613200,                "icon": "partly-cloudy-day",                "summary": "Partly Cloudy",                "sunriseTime": 1716635352,                "sunsetTime": 1716685284,                "moonPhase": 0.57,                "precipIntensity": 0.0,                "precipIntensityMax": 0.0,                "precipIntensityMaxTime": 1716613200,                "precipProbability": 0.0,                "precipAccumulation": 0.0,                "precipType": "none",                "temperatureHigh": 90.23,                "temperatureHighTime": 1716667200,                "temperatureLow": 75.15,                "temperatureLowTime": 1716714000,                "apparentTemperatureHigh": 100.69,                "apparentTemperatureHighTime": 1716663600,                "apparentTemperatureLow": 77.29,                "apparentTemperatureLowTime": 1716634800,                "dewPoint": 75.36,                "humidity": 0.8,                "pressure": 1009.95,                "windSpeed": 7.44,                "windGust": 15.32,                "windGustTime": 1716670800,                "windBearing": 178.7,                "cloudCover": 0.42,                "uvIndex": 8.67,                "uvIndexTime": 1716663600,                "visibility": 9.02,                "temperatureMin": 77.29,                "temperatureMinTime": 1716634800,                "temperatureMax": 90.23,                "temperatureMaxTime": 1716667200,                "apparentTemperatureMin": 77.29,                "apparentTemperatureMinTime": 1716634800,                "apparentTemperatureMax": 100.69,                "apparentTemperatureMaxTime": 1716663600            },            {                "time": 1716699600,                "icon": "partly-cloudy-day",                "summary": "Partly Cloudy",                "sunriseTime": 1716721730,                "sunsetTime": 1716771719,                "moonPhase": 0.6,                "precipIntensity": 0.0,                "precipIntensityMax": 0.0,                "precipIntensityMaxTime": 1716728400,                "precipProbability": 0.0,                "precipAccumulation": 0.0,                "precipType": "none",                "temperatureHigh": 88.85,                "temperatureHighTime": 1716753600,                "temperatureLow": 79.28,                "temperatureLowTime": 1716793200,                "apparentTemperatureHigh": 98.69,                "apparentTemperatureHighTime": 1716750000,                "apparentTemperatureLow": 74.33,                "apparentTemperatureLowTime": 1716717600,                "dewPoint": 74.87,                "humidity": 0.81,                "pressure": 1009.44,                "windSpeed": 11.02,                "windGust": 21.01,                "windGustTime": 1716750000,                "windBearing": 176.12,                "cloudCover": 0.39,                "uvIndex": 8.66,                "uvIndexTime": 1716750000,                "visibility": 8.14,                "temperatureMin": 75.15,                "temperatureMinTime": 1716714000,                "temperatureMax": 88.85,                "temperatureMaxTime": 1716753600,                "apparentTemperatureMin": 75.15,                "apparentTemperatureMinTime": 1716714000,                "apparentTemperatureMax": 98.69,                "apparentTemperatureMaxTime": 1716750000            },            {                "time": 1716786000,                "icon": "partly-cloudy-day",                "summary": "Partly Cloudy",                "sunriseTime": 1716808109,                "sunsetTime": 1716858153,                "moonPhase": 0.63,                "precipIntensity": 0.0,                "precipIntensityMax": 0.0,                "precipIntensityMaxTime": 1716786000,                "precipProbability": 0.14,                "precipAccumulation": 0.0,                "precipType": "rain",                "temperatureHigh": 90.81,                "temperatureHighTime": 1716840000,                "temperatureLow": 75.56,                "temperatureLowTime": 1716883200,                "apparentTemperatureHigh": 103.13,                "apparentTemperatureHighTime": 1716836400,                "apparentTemperatureLow": 73.34,                "apparentTemperatureLowTime": 1716800400,                "dewPoint": 75.65,                "humidity": 0.79,                "pressure": 1010.34,                "windSpeed": 6.62,                "windGust": 13.67,                "windGustTime": 1716786000,                "windBearing": 188.04,                "cloudCover": 0.42,                "uvIndex": 8.67,                "uvIndexTime": 1716836400,                "visibility": 9.9,                "temperatureMin": 78.95,                "temperatureMinTime": 1716868800,                "temperatureMax": 90.81,                "temperatureMaxTime": 1716840000,                "apparentTemperatureMin": 78.95,                "apparentTemperatureMinTime": 1716868800,                "apparentTemperatureMax": 103.13,                "apparentTemperatureMaxTime": 1716836400            },            {                "time": 1716872400,                "icon": "partly-cloudy-day",                "summary": "Partly Cloudy",                "sunriseTime": 1716894490,                "sunsetTime": 1716944587,                "moonPhase": 0.67,                "precipIntensity": 0.0,                "precipIntensityMax": 0.0,                "precipIntensityMaxTime": 1716872400,                "precipProbability": 0.22,                "precipAccumulation": 0.0,                "precipType": "rain",                "temperatureHigh": 90.44,                "temperatureHighTime": 1716922800,                "temperatureLow": 70.85,                "temperatureLowTime": 1716973200,                "apparentTemperatureHigh": 100.62,                "apparentTemperatureHighTime": 1716922800,                "apparentTemperatureLow": 74.34,                "apparentTemperatureLowTime": 1716894000,                "dewPoint": 73.67,                "humidity": 0.76,                "pressure": 1013.47,                "windSpeed": 3.86,                "windGust": 9.38,                "windGustTime": 1716922800,                "windBearing": 115.8,                "cloudCover": 0.39,                "uvIndex": 8.59,                "uvIndexTime": 1716922800,                "visibility": 10.0,                "temperatureMin": 75.56,                "temperatureMinTime": 1716883200,                "temperatureMax": 90.44,                "temperatureMaxTime": 1716922800,                "apparentTemperatureMin": 75.56,                "apparentTemperatureMinTime": 1716883200,                "apparentTemperatureMax": 100.62,                "apparentTemperatureMaxTime": 1716922800            },            {                "time": 1716958800,                "icon": "partly-cloudy-day",                "summary": "Partly Cloudy",                "sunriseTime": 1716980872,                "sunsetTime": 1717031021,                "moonPhase": 0.7,                "precipIntensity": 0.0157,                "precipIntensityMax": 0.0708,                "precipIntensityMaxTime": 1716958800,                "precipProbability": 0.19,                "precipAccumulation": 0.3758,                "precipType": "rain",                "temperatureHigh": 88.64,                "temperatureHighTime": 1717012800,                "temperatureLow": 71.5,                "temperatureLowTime": 1717063200,                "apparentTemperatureHigh": 93.01,                "apparentTemperatureHighTime": 1717009200,                "apparentTemperatureLow": 73.94,                "apparentTemperatureLowTime": 1716969600,                "dewPoint": 70.68,                "humidity": 0.76,                "pressure": 1015.88,                "windSpeed": 4.51,                "windGust": 10.26,                "windGustTime": 1717009200,                "windBearing": 104.64,                "cloudCover": 0.39,                "uvIndex": 7.05,                "uvIndexTime": 1717005600,                "visibility": 8.67,                "temperatureMin": 70.85,                "temperatureMinTime": 1716973200,                "temperatureMax": 88.64,                "temperatureMaxTime": 1717012800,                "apparentTemperatureMin": 70.85,                "apparentTemperatureMinTime": 1716973200,                "apparentTemperatureMax": 93.01,                "apparentTemperatureMaxTime": 1717009200            },            {                "time": 1717045200,                "icon": "partly-cloudy-day",                "summary": "Partly Cloudy",                "sunriseTime": 1717067256,                "sunsetTime": 1717117454,                "moonPhase": 0.74,                "precipIntensity": 0.0,                "precipIntensityMax": 0.0,                "precipIntensityMaxTime": 1717066800,                "precipProbability": 0.13,                "precipAccumulation": 0.0,                "precipType": "rain",                "temperatureHigh": 87.11,                "temperatureHighTime": 1717099200,                "temperatureLow": 71.89,                "temperatureLowTime": 1717146000,                "apparentTemperatureHigh": 90.42,                "apparentTemperatureHighTime": 1717099200,                "apparentTemperatureLow": 70.55,                "apparentTemperatureLowTime": 1717066800,                "dewPoint": 68.55,                "humidity": 0.73,                "pressure": 1015.01,                "windSpeed": 4.72,                "windGust": 10.78,                "windGustTime": 1717099200,                "windBearing": 113.73,                "cloudCover": 0.4,                "uvIndex": 5.79,                "uvIndexTime": 1717092000,                "visibility": 8.45,                "temperatureMin": 70.83,                "temperatureMinTime": 1717066800,                "temperatureMax": 87.11,                "temperatureMaxTime": 1717099200,                "apparentTemperatureMin": 70.83,                "apparentTemperatureMinTime": 1717066800,                "apparentTemperatureMax": 90.42,                "apparentTemperatureMaxTime": 1717099200            },            {                "time": 1717131600,                "icon": "clear-day",                "summary": "Clear",                "sunriseTime": 1717153641,                "sunsetTime": 1717203886,                "moonPhase": 0.78,                "precipIntensity": 0.0,                "precipIntensityMax": 0.0,                "precipIntensityMaxTime": 1717131600,                "precipProbability": 0.11,                "precipAccumulation": 0.0,                "precipType": "rain",                "temperatureHigh": 87.71,                "temperatureHighTime": 1717185600,                "temperatureLow": 75.94,                "temperatureLowTime": 1717218000,                "apparentTemperatureHigh": 91.18,                "apparentTemperatureHighTime": 1717185600,                "apparentTemperatureLow": 68.93,                "apparentTemperatureLowTime": 1717149600,                "dewPoint": 68.45,                "humidity": 0.7,                "pressure": 1014.01,                "windSpeed": 5.6,                "windGust": 12.33,                "windGustTime": 1717196400,                "windBearing": 128.78,                "cloudCover": 0.25,                "uvIndex": 7.18,                "uvIndexTime": 1717189200,                "visibility": 10.0,                "temperatureMin": 71.89,                "temperatureMinTime": 1717146000,                "temperatureMax": 87.71,                "temperatureMaxTime": 1717185600,                "apparentTemperatureMin": 71.89,                "apparentTemperatureMinTime": 1717146000,                "apparentTemperatureMax": 91.18,                "apparentTemperatureMaxTime": 1717185600            }        ]    },    "alerts": [],    "flags": {        "sources": [            "ETOPO1",            "gfs",            "gefs",            "hrrrsubh",            "hrrr_0-18",            "nbm",            "nbm_fire",            "hrrr_18-48"        ],        "sourceTimes": {            "hrrr_subh": "2024-05-24 13Z",            "hrrr_0-18": "2024-05-24 13Z",            "nbm": "2024-05-24 13Z",            "nbm_fire": "2024-05-24 06Z",            "hrrr_18-48": "2024-05-24 12Z",            "gfs": "2024-05-24 06Z",            "gefs": "2024-05-24 06Z"        },        "nearest-station": 0,        "units": "us",        "version": "V2.0.8"    }}

Still here? Thank you. Given all that data, you may want to summarize it a bit. Here's a script I wrote to do that:

const template = `{{#each days}}<p>Date: {{ date }}<br>Weather: {{ summary }}<br>High: {{ temperatureHigh }}F<br>Low: {{ temperatureLow}}F</p>{{/each}}`;function toDate(s) {    return new Date(s * 1000);}let result = pm.response.json();for(let d of result.daily.data) {    d.date = toDate(d.time);}pm.visualizer.set(template, { days: result.daily.data });

I take all that data and create a smaller summary showing the date, weather, and high and low temps. This renders out nicely like so:

Date: 2024-05-24T05:00:00.000ZWeather: ClearHigh: 89.11FLow: 77.63FDate: 2024-05-25T05:00:00.000ZWeather: Partly CloudyHigh: 90.23FLow: 75.15FDate: 2024-05-26T05:00:00.000ZWeather: Partly CloudyHigh: 88.85FLow: 79.28FDate: 2024-05-27T05:00:00.000ZWeather: Partly CloudyHigh: 90.81FLow: 75.56FDate: 2024-05-28T05:00:00.000ZWeather: Partly CloudyHigh: 90.44FLow: 70.85FDate: 2024-05-29T05:00:00.000ZWeather: Partly CloudyHigh: 88.64FLow: 71.5FDate: 2024-05-30T05:00:00.000ZWeather: Partly CloudyHigh: 87.11FLow: 71.89FDate: 2024-05-31T05:00:00.000ZWeather: ClearHigh: 87.71FLow: 75.94F

I think this feature would both be useful for showing other folks a high-level view of API results and heck, even just useful for yourself if you need to focus on a small portion of the data.

As cool as this is, I do want to share a warning. While I was building these examples, I made mistakes. That's natural. But I noticed sometimes Postman responded very badly to them, to the point where I had to close it and re-open it. I've got no idea why, I wasn't doing things like creating infinite loops, but for some reason, it would just stop responding well and only a restart of the app would help. I also noticed it sometimes "lost" edits my code. When I saw Postman start acting up, I'd copy the code over to Notepad, restart, and paste it back in. I'm sure it was my fault but... keep it in mind.

As before, I've got a video version of this as well. Enjoy!

More News from this Feed See Full Web Site