Having Coffee with Deno - Dynamic Names
Welcome to the second installment of our Deno series, where we build a script that pairs up people for coffee.
In the last post, we built a script that helped the Justice League meet up for coffee.
As of right now, our script looks like the following.
Even though this approach works, the major problem is that every time there's a member change in the Justice League (which seems to happen more often than not), we have to go back and update the list manually.
It'd be better if we could get this list dynamically instead. Given that the League are great developers, they have their own GitHub organization. Let's work on integrating with GitHub's API to get the list of names.
The Approach
To get the list of names from GitHub, we'll need to do the following.
- First, we need to figure out which GitHub endpoint will give us the members of the League. This, in turn, will also tell us what permissions we need for our API scope.
- Now that we have a secret, we need to update our script to read from an
.env
file. - Once we have the secret being read, we can create a function to retrieve the members of the League.
- Miscellaneous refactoring of the main script to handle a function returning complex types instead of strings.
Laying the Foundation
Before we start, we should reactor our current file. It works, but we have a mix of utility functions (shuffle
and createPairsFrom
) combined with presentation functions (createMessage
). Let's go ahead and move shuffle
and createPairsFrom
to their own module.
With these changes, we can update index.ts
to be:
Getting GitHub
Now that our code is more tidy, we can focus on figuring out which GitHub endpoint(s) to use to figure out the members of the Justice League.
Taking a look at the docs, we see that there are two different options.
What's the difference between the two? In GitHub parlance, an Organization is an overarching entity that consists of members which, in turn, can be part of multiple teams.
Using the Justice League as an example, it's an organization that contains Batman, and Batman can be part of the Justice League Founding Team and a member of the Batfamily Team.
Since we want to pair everyone up in the Justice League, we'll use the Get members of an Organization approach.
Working with Secrets
To interact with the endpoint, we'll need to create an API token for GitHub. Looking over the docs, our token needs to have the read:org
scope. We can create this token by following the instructions here about creating a Personal Auth Token (PAT).
Once we have the token, we can invoke the endpoint with cURL or Postman to verify that we can communicate with the endpoint correctly.
After we've verified, we'll need a way to get this API token into our script. Given that this is sensitive information, we absolutely should NOT check this into the source code.
Creating an ENV File
A common way of dealing with that is to use an .env file which doesn't get checked in, but our application can use it during runtime to get secrets.
Let's go ahead and create the .env
file and put our API token here.
.env | |
---|---|
Our problem now is that if we check git status
, we'll see this file listed as a change. We don't want to check this in, so let's add a .gitignore
file.
Adding a .gitignore File
With the .env
file created, we need to create a .gitignore file, which tells Git not to check in certain files.
Let's go ahead and add the file. You can enter the below, or you can use the Node gitignore file (found here)
.gitignore | |
---|---|
We can validate that we've done this correctly if we run git status
and don't see .env
showing up anymore as a changed file.
Loading Our Env File
Now that we have the file created, we need to make sure that this file loads at runtime.
In our index.ts
file, let's make the following changes.
When we run the script now with deno run
, we get an interesting prompt:
Deno requests read access to ".env".
- Requested by `Deno.readFileSync()` API.
- Run again with --allow-read to bypass this prompt
- Allow?
This is one of the coolest parts about Deno; it has a security system that prevents scripts from doing something that you hadn't intended through its Permissions framework.
For example, if you weren't expecting your script to read from the env
file, it'll prompt you to accept. Since packages can be taken over and updated to do nefarious things, this is a terrific idea.
The permissions can be tuned (e.g., you're only allowed to read from the .env file), or you can give blanket permissions. In our cases, two resources are being used (the ability to read the .env
file and the ability to read the GITHUB_BEARER_TOKEN
environment variable).
Let's run the command with the allow-read
and allow-env
flags.
deno run --allow-run --allow-env ./index.ts
If the bearer token gets printed, we've got the .env
file created correctly and can proceed to the next step.
Let's Get Dynamic
Now that we have the bearer token, we can work on calling the GitHub Organization endpoint to retrieve the members.
Since this is GitHub related, we should create a new file, github.ts
, to host our functions and types.
Adding axiod
In the github.ts
file, we're going to be use axiod for communication. It's similar to axios in Node and is better than then the built-in fetch API.
Let's go ahead and bring in the import.
github.ts | |
---|---|
Calling the Organization Endpoint
With axiod
pulled in, let's write the function to interact with the GitHub API.
To prove this is working, we can call this function in the index.ts
file and verify that we're getting a response.
Now let's rerun the script.
Deno requests net access to "api.github.com"
- Requested by `fetch` API.
- Run again with --allow-net to bypass this prompt.
Ah! Our script is now doing something new (making network calls), so we'll need to allow that permission by using the --allow-net
flag.
If everything has worked, you should see a bunch of JSON printed to the screen. Success!
Creating the Response Type
At this point, we're making the call, but we're using a pesky any
for the response, which works, but it doesn't help us with what properties we have to work with.
Looking at the response schema, it seems the main field we need is login
. So let's go ahead and create a type that includes that field.
github.ts | |
---|---|
We can rerun our code and verify that everything is still working, but now with better type safety.
Cleaning Up
Now that we have this function written, we can work to integrate it with our index.ts
script.
So far, so good. The only change we had to make was to replace the hardcoded array of names with the call to getMembersOfOrganization
.
Not an issue, right?
Hmmm, what's up with this?
It looks like createMessage
is expecting Pair<string>[]
, but is receiving Pair<GetOrganizationMemberResponse>[]
.
To solve this problem, we'll modify createMessage
to work with GetOrganizationMemberResponse
.
With this last change, we run the script and verify that we're getting the correct output, huzzah!
Current Status
Congratulations! We now have a script that is dynamically pulling in heroes from the Justice League organization instead of always needing to see if Green Lantern is somewhere else or if another member of Flash's SpeedForce is here for the moment.
A working version of the code can be found on GitHub.