diff --git a/.env.example b/.env.example index 9bbe248..c4627ac 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4fc7849..a48c9bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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: diff --git a/authentication.js b/authentication.js index eab68da..20d5433 100644 --- a/authentication.js +++ b/authentication.js @@ -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], }; diff --git a/creates/create_casegoods_adjustment.js b/creates/create_casegoods_adjustment.js index 73264f7..d8c4f62 100644 --- a/creates/create_casegoods_adjustment.js +++ b/creates/create_casegoods_adjustment.js @@ -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': { @@ -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: { diff --git a/fields/list_analysistypes_dropdown.js b/fields/list_analysistypes_dropdown.js index 8dcb0be..ec0a82d 100644 --- a/fields/list_analysistypes_dropdown.js +++ b/fields/list_analysistypes_dropdown.js @@ -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'; @@ -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]; diff --git a/fields/list_appellations_dropdown.js b/fields/list_appellations_dropdown.js index ec91025..1106660 100644 --- a/fields/list_appellations_dropdown.js +++ b/fields/list_appellations_dropdown.js @@ -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'; @@ -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, diff --git a/fields/list_drygoodtypes_dropdown.js b/fields/list_drygoodtypes_dropdown.js index 2b56810..9522bd6 100644 --- a/fields/list_drygoodtypes_dropdown.js +++ b/fields/list_drygoodtypes_dropdown.js @@ -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'; @@ -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]; diff --git a/fields/list_varietals_dropdown.js b/fields/list_varietals_dropdown.js index fa35b62..1dfdbaa 100644 --- a/fields/list_varietals_dropdown.js +++ b/fields/list_varietals_dropdown.js @@ -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} - 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'; @@ -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]; diff --git a/fields/list_wineries_dropdown.js b/fields/list_wineries_dropdown.js index 7d599c6..b94d09d 100644 --- a/fields/list_wineries_dropdown.js +++ b/fields/list_wineries_dropdown.js @@ -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, }, }; diff --git a/index.js b/index.js index e8837d6..3af233b 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,3 @@ -const { - config: authentication, - befores = [], - afters = [], -} = require('./authentication'); - // Actions // Searches @@ -30,6 +24,12 @@ const createCaseGoodsAdjustment = // Resources +const { + config: authentication, + befores = [], + afters = [], +} = require('./authentication'); + const App = { // This is just shorthand to reference the installed dependencies you have. // Zapier will need to know these before we can upload diff --git a/searches/get_casegoods_lotid.js b/searches/get_casegoods_lotid.js index 620539f..f02149e 100644 --- a/searches/get_casegoods_lotid.js +++ b/searches/get_casegoods_lotid.js @@ -10,12 +10,19 @@ const getCaseGoodsLotId = async (z, bundle) => { const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', - 'Authorization': `Access-Token ${bundle.authData.accessToken}`, + 'Authorization': `Access-Token ${bundle.authData.apiKey}`, }; const url = `https://sutter.innovint.us/api/v1/wineries/${bundle.inputData.wineryId}/lots`; - const responseData = await z.request(url, 'GET', - {limit: 1, q: bundle.inputData.caseGoodsName}, headers); + + const options = { + params: { + limit: 1, + q: bundle.inputData.caseGoodsName, + }, + }; + + const responseData = await z.request(url, options, headers); if (responseData) { if (responseData.status === 200) { @@ -73,7 +80,12 @@ module.exports = { operation: { perform: getCaseGoodsLotId, inputFields: [ - {key: 'wineryId', required: true, type: 'string'}, + { + key: 'wineryId', + required: true, + type: 'string', + dynamic: 'listWineriesDropdown.id.name', + }, {key: 'caseGoodsName', required: true, type: 'string'}, ], sample: {id: 'lot_Z1LPW8OQMY23L6QM3KXJD45Y'}, diff --git a/test/_bundle.js b/test/_bundle.js index 0b75440..b6fb132 100644 --- a/test/_bundle.js +++ b/test/_bundle.js @@ -1,6 +1,6 @@ const bundle = { authData: { - accessToken: process.env.TEST_TOKEN || '', + apiKey: process.env.TEST_API_KEY || '', }, inputData: { wineryId: '123', // Common winery ID for tests diff --git a/test/authentication.test.js b/test/authentication.test.js index 1224eea..d304f1f 100644 --- a/test/authentication.test.js +++ b/test/authentication.test.js @@ -1,5 +1,4 @@ const should = require('should'); -const authentication = require('../authentication'); const {bundle} = require('./_bundle'); const zapier = require('zapier-platform-core'); @@ -10,20 +9,23 @@ const appTester = zapier.createAppTester(App); describe('Authentication tests', () => { it('should authenticate successfully with valid credentials', async () => { - // Use the bundle as is, assuming TEST_TOKEN is set correctly - await appTester(authentication.test, bundle); - // Add assertions as necessary + // Use the bundle as is, assuming TEST_API_KEY is set correctly + const response = await appTester(App.authentication.test, bundle); + + // Assert that the response is successful and contains expected data + should(response.status).equal(200); // Assuming a status 200 for successful authentication + // Add more specific assertions based on the expected response structure }); it('should fail authentication with invalid credentials', async () => { const invalidBundle = { authData: { - accessToken: 'invalid_token', + apiKey: 'invalid_token', }, }; try { - await appTester(authentication.test, invalidBundle); + await appTester(App.authentication.test, invalidBundle); // If no error is thrown, use should to assert failure should(false).be.true('Expected an error, but none was thrown'); } catch (error) { diff --git a/test/unit/fields/list_analysistypes_dropdown.test.js b/test/unit/fields/list_analysistypes_dropdown.test.js index 2bc0072..72ae3eb 100644 --- a/test/unit/fields/list_analysistypes_dropdown.test.js +++ b/test/unit/fields/list_analysistypes_dropdown.test.js @@ -59,19 +59,8 @@ describe('List Analysis Types Trigger', () => { const results = await appTester(listAnalysisTypes.operation.perform, bundle); - - should(results).containEql({ - id: 'wine-color-density', - name: 'Wine Color Density', - abbreviation: 'Color-Density', - units: 'Absorbance Units', - }); - - should(results).containEql({ - id: 'nitrogen', - name: 'Nitrogen', - abbreviation: '', - units: 'Milligrams per Liter, Parts per Million', - }); + should(results).containEql( + {id: 'wine-color-density', name: 'Wine Color Density'}); + should(results).containEql({id: 'nitrogen', name: 'Nitrogen'}); }); }); diff --git a/test/unit/fields/list_drygoodtypes_dropdown.test.js b/test/unit/fields/list_drygoodtypes_dropdown.test.js index aa4693d..1261485 100644 --- a/test/unit/fields/list_drygoodtypes_dropdown.test.js +++ b/test/unit/fields/list_drygoodtypes_dropdown.test.js @@ -80,31 +80,14 @@ describe('List Dry Goods Trigger', () => { ); const results = await appTester(listDryGoodTypes.operation.perform, bundle); - const expectedFirstResult = { id: 'dgt_9P2VQ0D3NK7LZMZ6WROJ81E4', name: 'Acid', - category: 'Additive', - units: { - all: - ['L', 'mL', 'gal', 'dL', 'hL', 'kgal', 'g', 'mg', 'lb', 'kg', 'ton', - ], - liquid: ['L', 'mL', 'gal', 'dL', 'hL', 'kgal'], - dry: ['g', 'mg', 'lb', 'kg', 'ton'], - }, }; - const expectedSecondResult = { id: 'dgt_1JEW56RPYO7JRMVXK8QDZ3L9', name: 'Boxes', - category: 'Packaging', - units: { - all: ['boxes', 'inserts'], - liquid: [], - dry: [], - }, }; - should(results).containDeep([expectedFirstResult, expectedSecondResult]); }); }); diff --git a/test/unit/fields/list_varietals_dropdown.test.js b/test/unit/fields/list_varietals_dropdown.test.js index 10a32be..749959c 100644 --- a/test/unit/fields/list_varietals_dropdown.test.js +++ b/test/unit/fields/list_varietals_dropdown.test.js @@ -47,8 +47,8 @@ describe('List Varietals Trigger', () => { const results = await appTester(listVarietals.operation.perform, bundle); should(results).containDeep( - [{id: 'vari_LZ4E0PWYDM8X8LGX6R92JK51', name: 'Acolon', color: 'red'}]); + [{id: 'vari_LZ4E0PWYDM8X8LGX6R92JK51', name: 'Acolon'}]); should(results).containDeep( - [{id: 'vari_9P2VQ0D3NK7LZMZ6WROJ81E4', name: 'Agawam', color: 'red'}]); + [{id: 'vari_9P2VQ0D3NK7LZMZ6WROJ81E4', name: 'Agawam'}]); }); }); diff --git a/test/unit/fields/list_wineries_dropdown.test.js b/test/unit/fields/list_wineries_dropdown.test.js index b932648..9136ced 100644 --- a/test/unit/fields/list_wineries_dropdown.test.js +++ b/test/unit/fields/list_wineries_dropdown.test.js @@ -1,12 +1,12 @@ +const should = require('should'); const zapier = require('zapier-platform-core'); const nock = require('nock'); const App = require('../../../index'); const appTester = zapier.createAppTester(App); const {bundle} = require('../../_bundle'); -const listWineries = require('../../../fields/list_wineries_dropdown'); +const listWineriesDropdown = require('../../../fields/list_wineries_dropdown'); describe('List Wineries Trigger', () => { - // Use this to mock environment variables zapier.tools.env.inject(); it('should correctly handle pagination when loading wineries', async () => { @@ -14,34 +14,48 @@ describe('List Wineries Trigger', () => { nock('https://sutter.innovint.us') .get('/api/v1/wineries') .reply(200, { - pagination: { - next: 'https://sutter.innovint.us/api/v1/wineries/next', - }, results: [ - {data: {internalId: 1, id: 'string1', name: 'Winery 1'}}, - // Additional results for page 1... + { + data: {id: 'wnry_1', internalId: 1001, name: 'Winery One'}, + // ... other properties + }, + { + data: {id: 'wnry_2', internalId: 1002, name: 'Winery Two'}, + // ... other properties + }, ], + pagination: { + next: 'https://sutter.innovint.us/api/v1/wineries?page=2', + }, }); - // Mock the second (next) page + // Mock the second page nock('https://sutter.innovint.us') - .get('/api/v1/wineries/next') + .get('/api/v1/wineries?page=2') .reply(200, { + results: [ + { + data: {id: 'wnry_3', internalId: 1003, name: 'Winery Three'}, + // ... other properties + }, + { + data: {id: 'wnry_4', internalId: 1004, name: 'Winery Four'}, + // ... other properties + }, + ], pagination: { next: null, }, - results: [ - {data: {internalId: 3, id: 'string3', name: 'Winery 3'}}, - // Additional results for page 2... - ], }); - const results = await appTester(listWineries.operation.perform, bundle); + const results = await appTester(listWineriesDropdown.operation.perform, + bundle); - // Check if results include items from all pages using should - // Ensure results contain data from both the first and second page - results.should.containEql({id: '1'}); - results.should.containEql({id: '3'}); - // Additional assertions as needed... + // Check if results include items from all pages + should(results).have.length(4); + results.forEach((result) => { + should(result).have.property('id'); // Check for label instead of id + should(result).have.property('name'); // Check for value instead of id + }); }); });