Skip to content

Commit

Permalink
Refactor authentication and improve test cases
Browse files Browse the repository at this point in the history
This commit refactors the authentication mechanism from using 'accessToken' to 'apiKey', reflecting these changes in relevant API calls. It also enhances various test cases by simplifying assertions, removing unnecessary attributes, and improving overall code readability. In addition, certain functions and field descriptions have been updated for clarity.
  • Loading branch information
tech-consortium committed Dec 17, 2023
1 parent bbe3645 commit c654b89
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 168 deletions.
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
ACCESS_TOKEN=your_personal_access_token_here
API_KEY=your_personal_access_token_here

TEST_TOKEN=personal_access_token_for_tests
TEST_API_KEY=personal_access_token_for_tests
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
- name: create env file
run: |
touch .env
echo TEST_TOKEN=${{ secrets.TEST_TOKEN }} >> .env
echo TEST_API_KEY=${{ secrets.TEST_API_KEY }} >> .env
- name: Use Node.js
uses: actions/setup-node@v3
with:
Expand Down
118 changes: 71 additions & 47 deletions authentication.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,81 @@
require('dotenv').config(); // Loads variables from .env file

// The test call Zapier makes to ensure an access token is valid
// UX TIP: Hit an endpoint that always returns data with valid credentials,
// like a /profile or /me endpoint. That way the success/failure is related to
// the token and not because the user didn't happen to have a recently created
// record.
const testAuth = (z, bundle) => {
const testUrl = 'https://sutter.innovint.us/api/v1/wineries';

// Use the accessToken from bundle for authentication
const accessToken = bundle.authData.accessToken || process.env.ACCESS_TOKEN;

return z.request({
url: testUrl,
method: 'GET',
headers: {
'Authorization': `Access-Token ${accessToken}`,
},
}).then((response) => {
if (
response.status !== 200 ||
response.content.includes('some_error_indicator')) {
throw new Error('The provided Access Token is invalid.');
}
return response;
});
'use strict';

require('dotenv').config();

// You want to make a request to an endpoint that is either specifically designed
// to test auth, or one that every user will have access to. eg: `/me`.
// By returning the entire request object, you have access to the request and
// response data for testing purposes. Your connection label can access any data
// from the returned response using the `json.` prefix. eg: `{{json.username}}`.
const test = (z, bundle) =>
z.request({url: 'https://sutter.innovint.us/api/v1/wineries'});

// This function runs after every outbound request. You can use it to check for
// errors or modify the response. You can have as many as you need. They'll need
// to each be registered in your index.js file.
const handleBadResponses = (response, z, bundle) => {
if (response.status === 401) {
throw new z.errors.Error(
// This message is surfaced to the user
'The API Key you supplied is incorrect',
'AuthenticationError',
response.status,
);
}

return response;
};

const includeAccessTokenHeader = (request, z, bundle) => {
// Prioritize the token from the bundle, fall back to the environment variable
const accessToken = bundle.authData.accessToken || process.env.ACCESS_TOKEN;
// This function runs before every outbound request. You can have as many as you
// need. They'll need to each be registered in your index.js file.
const includeApiKey = (request, z, bundle) => {
// Use API key from bundle.authData, or fallback to the one from .env
const apiKey = bundle.authData.apiKey || process.env.API_KEY;

if (accessToken) {
request.headers = request.headers || {};
request.headers['Authorization'] = `Access-Token ${accessToken}`;
if (apiKey) {
// Use these lines to include the API key in the querystring
// request.params = request.params || {};
// request.params.api_key = bundle.authData.apiKey;

// If you want to include the API key in the header instead, uncomment this:
request.headers.Authorization = `Access-Token ${apiKey}`;
}

return request;
};

module.exports = {
type: 'custom',
connectionLabel: '{{bundle.authData.connectionLabel}}',
fields: [
{
key: 'accessToken',
label: 'Access Token',
required: true,
type: 'string',
helpText: 'Your personal access token for the API.',
},
],
test: testAuth,
befores: [includeAccessTokenHeader],
afters: [],
config: {
// "custom" is the catch-all auth type. The user supplies some info and Zapier can
// make authenticated requests with it
type: 'custom',

// Define any input app's auth requires here. The user will be prompted to enter
// this info when they connect their account.
fields: [
{
key: 'apiKey',
label: 'API Key',
required: true,
type: 'string',
helpText: 'Go to the [API Details](https://cellar.innovint.us/#/developer/personal-access-token) ' +
'page in your account settings to find your Personal Access Tokens for the API Key.',
},
],

// The test method allows Zapier to verify that the credentials a user provides
// are valid. We'll execute this method whenever a user connects their account for
// the first time.
test,

// This template string can access all the data returned from the auth test. If
// you return the test object, you'll access the returned data with a label like
// `{{json.X}}`. If you return `response.data` from your test, then your label can
// be `{{X}}`. This can also be a function that returns a label. That function has
// the standard args `(z, bundle)` and data returned from the test can be accessed
// in `bundle.inputData.X`.
connectionLabel: '{{json.username}}',
},
befores: [includeApiKey],
afters: [handleBadResponses],
};
6 changes: 3 additions & 3 deletions creates/create_casegoods_adjustment.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const perform = async (z, bundle) => {
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Access-Token ${bundle.authData.api_key}`,
'Authorization': `Access-Token ${bundle.authData.apiKey}`,
},
body: JSON.stringify({
'data': {
Expand Down Expand Up @@ -35,10 +35,10 @@ const perform = async (z, bundle) => {
};

module.exports = {
key: 'someAction',
key: 'createCaseGoodsAdjustment',
noun: 'Action',
display: {
label: 'Perform Some Action',
label: 'Case Goods Adjustment',
description: 'Performs an action using the Lot ID.',
},
operation: {
Expand Down
15 changes: 11 additions & 4 deletions fields/list_analysistypes_dropdown.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* Retrieves a list of analysis types from the API.
*
* @param {object} z - The 'z' object from Zapier which provides access to built-in actions and resources.
* @param {object} bundle - The bundle object which contains information about the current Zap instance.
* @return {array} - An array of analysis types.
*/
const listAnalysisTypesDropdown = async (z, bundle) => {
let analysistypes = [];
let nextPageUrl = 'https://sutter.innovint.us/api/v1/analysisTypes';
Expand All @@ -8,16 +15,16 @@ const listAnalysisTypesDropdown = async (z, bundle) => {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Access-Token ${bundle.authData.accessToken}`,
'Authorization': `Access-Token ${bundle.authData.apiKey}`,
},
});

const responseData = response.json;
response.throwForStatus();
const responseData = await response.json;

const pageAnalysisTypes = responseData.results.map((item) => ({
id: item.data.slug, // Use 'slug' as the 'id'
name: item.data.name,
abbreviation: item.data.abbreviation,
units: item.data.units.map((unit) => unit.name).join(', '),
}));

analysistypes = [...analysistypes, ...pageAnalysisTypes];
Expand Down
15 changes: 12 additions & 3 deletions fields/list_appellations_dropdown.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* Retrieves a list of appellations from a REST API.
*
* @async
* @param {Object} z - The z object from the zapier library.
* @param {Object} bundle - The bundle object containing authorization data.
* @return {Array} - An array of appellations.
*/
const listAppellationsDropdown = async (z, bundle) => {
let appellations = [];
let nextPageUrl = 'https://sutter.innovint.us/api/v1/appellations';
Expand All @@ -8,12 +16,13 @@ const listAppellationsDropdown = async (z, bundle) => {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Access-Token ${bundle.authData.accessToken}`,
'Authorization': `Access-Token ${bundle.authData.apiKey}`,
},
});

// Assuming the structure of the response is as you described
const responseData = response.json;
response.throwForStatus();
const responseData = await response.json;

const pageAppellations = responseData.results.map((item) => ({
id: item.data.id,
name: item.data.name,
Expand Down
19 changes: 11 additions & 8 deletions fields/list_drygoodtypes_dropdown.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
/**
* Retrieves a list of dry goods types from the Sutter API.
*
* @param {Object} z - The underlying Zapier `z` object.
* @param {Object} bundle - The Zapier `bundle` object with authentication data.
* @return {Array} - An array of dry goods types with their respective properties.
*/
const listDryGoodTypesDropdown = async (z, bundle) => {
let drygoodtypes = [];
let nextPageUrl = 'https://sutter.innovint.us/api/v1/dryGoodTypes';
Expand All @@ -8,20 +15,16 @@ const listDryGoodTypesDropdown = async (z, bundle) => {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Access-Token ${bundle.authData.accessToken}`,
'Authorization': `Access-Token ${bundle.authData.apiKey}`,
},
});

const responseData = response.json;
response.throwForStatus();
const responseData = await response.json;

const pageDryGoods = responseData.results.map((item) => ({
id: item.data.id,
name: item.data.name,
category: item.data.category,
units: {
all: item.data.units.all || [],
liquid: item.data.units.liquid || [],
dry: item.data.units.dry || [],
},
}));

drygoodtypes = [...drygoodtypes, ...pageDryGoods];
Expand Down
16 changes: 12 additions & 4 deletions fields/list_varietals_dropdown.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
/**
* Fetches a list of varietals from the specified API endpoint.
*
* @async
* @param {Object} z - The z object provided by the Zapier platform.
* @param {Object} bundle - The bundle object provided by the Zapier platform.
* @return {Promise<Object[]>} - A promise that resolves to an array of varietals.
*/
const listVarietalsDropdown = async (z, bundle) => {
let varietals = [];
let nextPageUrl = 'https://sutter.innovint.us/api/v1/varietals';
Expand All @@ -8,16 +16,16 @@ const listVarietalsDropdown = async (z, bundle) => {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Access-Token ${bundle.authData.accessToken}`,
'Authorization': `Access-Token ${bundle.authData.apiKey}`,
},
});

// Assuming the structure of the response is as you described
const responseData = response.json;
response.throwForStatus();
const responseData = await response.json;

const pageVarietals = responseData.results.map((item) => ({
id: item.data.id,
name: item.data.name,
color: item.data.color,
}));

varietals = [...varietals, ...pageVarietals];
Expand Down
70 changes: 43 additions & 27 deletions fields/list_wineries_dropdown.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,57 @@
const listWineries = (z, bundle, url) => {
const options = {
url: url,
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Access-Token ${bundle.authData.accessToken}`,
},
};
/**
* Retrieves a list of wineries for a dropdown.
* @async
* @param {Object} z - The 'zapier' object.
* @param {Object} bundle - The bundle containing additional data.
* @return {Array} - An array of wineries for the dropdown.
*/
const listWineriesDropdown = async (z, bundle) => {
let wineries = [];
let nextPageUrl = 'https://sutter.innovint.us/api/v1/wineries';

return z.request(options)
.then((response) => {
response.throwForStatus();
const results = response.json;
while (nextPageUrl) {
const response = await z.request({
url: nextPageUrl,
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Access-Token ${bundle.authData.apiKey}`,
},
});

const pageWineryIds = results.results.map(
(winery) => ({id: winery.data.internalId.toString()}));
response.throwForStatus();
const responseData = await response.json;

// If there's a next page, recursively fetch it
if (results.pagination.next) {
return listWineries(z, bundle, results.pagination.next).then(
(nextPageWineryIds) => pageWineryIds.concat(nextPageWineryIds));
} else {
return pageWineryIds;
}
});
const pageWineries = responseData.results.map((winery) => ({
id: winery.data.id,
name: winery.data.name,
}));

wineries = [...wineries, ...pageWineries];
nextPageUrl = responseData.pagination.next;
}

return wineries;
};

module.exports = {
key: 'listWineries',
key: 'listWineriesDropdown',
noun: 'Winery',
display: {
label: 'List Wineries',
description: 'Returns a list of wineries.',
description: 'Trigger for field dropdown of Wineries.',
hidden: true,
},
operation: {
perform: (z, bundle) => listWineries(z, bundle,
'https://sutter.innovint.us/api/v1/wineries'),
inputFields: [
{
key: 'wineryId',
label: 'Select a Winery',
type: 'string',
dynamic: 'listWineriesDropdown.id.label',
},
],
perform: listWineriesDropdown,
canPaginate: true,
},
};
Loading

0 comments on commit c654b89

Please sign in to comment.