Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,27 @@ Audit history will track the token used to make the change, and will display in
- `entity_id` the vendorful id of the legal entity you wish to update
- `attribute_id` the vendorful id of the attribute you wish to update
- `value` the new value

## Intake forms

### /GET

See it in action: `yarn run get-intake-form organization_id form_id`

This command assumes you have a `secrets.json` file with appropriate credentials. See [secrets-sample.json](./secrets-sample.json) for the format.

The `organization_id` is the ID of the organization that created the intake form.
The `form_id` is the ID of the intake form.

The response returned can be populated with values and used for the /POST with no additional changes required.

### /POST

See it in action: `yarn run create-intake-form-response organization_id form_id`

This command assumes you have a `secrets.json` file with appropriate credentials. See [secrets-sample.json](./secrets-sample.json) for the format.

The `organization_id` is the ID of the organization that created the intake form.
The `form_id` is the ID of the intake form.

The field values can be hardcoded in `/src/intake-forms/create-intake-forms`. See that file for an example.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"get-token-subject": "node ./src/auth/get-token-subject.js",
"expire-token": "node ./src/auth/expire-token.js",
"sync-vendors": "node ./src/vendors/sync.js",
"set-attribute-value": "node ./src/vendors/set-attribute-value.js"
"set-attribute-value": "node ./src/vendors/set-attribute-value.js",
"get-intake-form": "node ./src/intake-forms/get-intake-form.js",
"create-intake-form-response": "node ./src/intake-forms/create-intake-form-response.js"
},
"type": "module",
"dependencies": {
Expand Down
6 changes: 6 additions & 0 deletions src/api_urls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const API_VERSION = "v1";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API versioning is per-module, so it will make more sense, I think, to include it as part of the opaque string that makes up the base url. It is very likely that the version numbers will increment differently for each API. The Auth API will likely never increment, but if we introduce a breaking change in the Vendor API, we would not need to increment all API versions nor deprecate URLs for other modules.

The TOKEN_ENDPOINT itself is a special case, as that is the name given in the OAuth2 specs, in that it is an opaque value to callers of the API that hopefully they would just paste into their OAuth2 client libraries that manage token expiration and renewal for them.

const BASE_URL = "https://api.vendorful.com";

export const TOKEN_ENDPOINT = `${BASE_URL}/auth/${API_VERSION}/token`;
export const VENDORS_API = `${BASE_URL}/vendors/${API_VERSION}`;
export const INTAKE_FORMS_API = `${BASE_URL}/intake-forms/${API_VERSION}`;
7 changes: 4 additions & 3 deletions src/auth/create-token-with-client-credentials.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fetch from "node-fetch";
import { TOKEN_ENDPOINT } from "./../api_urls.js";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the primary purpose of this code is to serve as a document to communicate how to call the API, I think overt code is preferable, even if duplicative.


/*
* Creating a token with grant_type client_credentials can be
Expand All @@ -15,8 +16,6 @@ import fetch from "node-fetch";
* contacting vendorful support.
*/

const TOKEN_ENDPOINT = "https://api.vendorful.com/auth/v1/token";

async function createToken(id, secret, username) {
const params = {
grant_type: "client_credentials",
Expand All @@ -38,7 +37,9 @@ async function main() {
const email = process.argv[4];
if (!secret) {
console.log("Please pass client_id and client_secret.");
console.log("> yarn run create-token-with-client-credentials <client_id> <client_secret> <optional_user_email>");
console.log(
"> yarn run create-token-with-client-credentials <client_id> <client_secret> <optional_user_email>"
);
} else {
try {
const token = await createToken(id, secret, email);
Expand Down
3 changes: 1 addition & 2 deletions src/auth/create-token-with-password.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import fetch from "node-fetch";
import readline from "readline";
import { TOKEN_ENDPOINT } from "./../api_urls.js";

/*
* Creating a token with grant_type password is as easy as
* POSTing JSON with username, password, and grant_type
* to the token endpoint.
*/

const TOKEN_ENDPOINT = "https://api.vendorful.com/auth/v1/token";

async function createPasswordToken(username, password) {
const response = await fetch(TOKEN_ENDPOINT, {
method: "POST",
Expand Down
3 changes: 1 addition & 2 deletions src/auth/expire-token.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import fetch from "node-fetch";
import { TOKEN_ENDPOINT } from "./../api_urls.js";

/*
* Tokens can be expired early by calling DELETE on the token
* endpoint using the authorization header like you would any
* other authenticated call.
*/

const TOKEN_ENDPOINT = "https://api.vendorful.com/auth/v1/token";

async function expireToken(access_token) {
const response = await fetch(TOKEN_ENDPOINT, {
method: "DELETE",
Expand Down
143 changes: 143 additions & 0 deletions src/intake-forms/create-intake-form-response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import fetch from "node-fetch";
import { readFile } from "fs/promises";
import { TOKEN_ENDPOINT, INTAKE_FORMS_API } from "./../api_urls.js";

// Gets the token by posting secrets.json to the token endpoint.
// See secrets-sample.json for an example.
async function getToken() {
const response = await fetch(TOKEN_ENDPOINT, {
method: "post",
body: await readFile("secrets.json"),
headers: { "content-type": "application/json" },
});
return await response.json();
}

async function createIntakeFormResponse(
{ access_token },
organizationId,
formId,
fields
) {
const url = `${INTAKE_FORMS_API}/${organizationId}/intake-forms/${formId}`;
const response = await fetch(url, {
method: "post",
body: JSON.stringify({ fields }),
headers: {
authorization: `bearer ${access_token}`,
"content-type": "application/json",
},
});

return await response.json();
}

// Example of fields from /GET response with "value" added
// Please see and run get-intake-forms.js to retrieve these field ids
// from the http response's body.
const fields = [
{
body: null,
id: "874d5c18-c806-4688-babc-28b50b457bda",
multi: null,
name: "Short Text Test",
options: [],
required: false,
type: "string",
value: "test",
},
{
body: null,
id: "649e1fc1-1950-415d-b97e-a89f3e865969",
multi: null,
name: "Expanded Text Test",
options: [],
required: false,
type: "textarea",
value: "test",
},
{
body: null,
id: "d5d79f93-9c6f-4960-b5d0-e9bc5fa6a176",
multi: null,
name: "Checkbox UI Test",
options: [],
required: false,
type: "checkbox",
value: true,
},
{
body: null,
id: "d72fb676-6488-4404-9d89-1cd863031ec5",
multi: null,
name: "Single Select Test: Sandwich Choice",
options: ["Ham", "Roast Beef", "Pimento Cheese"],
required: false,
type: "select",
value: "Roast Beef",
},
{
body: null,
id: "7d0a3375-3642-44be-9b9b-a9f6b6da26b3",
multi: true,
name: "Multi-Select Test: Cookie Options",
options: ["Snickerdoodle", "Chocolate Chip", "Peanut Butter"],
required: false,
type: "select",
value: ["Snickerdoodle", "Chocolate Chip"],
},
{
body: null,
id: "4b7ebdff-afb3-484a-96e7-c7adb5ddab8a",
multi: null,
name: "Number Test",
options: [],
required: false,
type: "number",
value: 5,
},
{
body: null,
id: "7b9be557-5299-447c-8b5f-07214c3ea183",
multi: null,
name: "Range Test",
options: ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"],
required: false,
type: "range",
value: "4",
},
{
body: null,
id: "f8b048f2-ab5d-4391-84b5-2c7b0a077552",
multi: null,
name: "Date Time Test",
options: [],
required: false,
type: "datetime",
value: new Date(),
},
];

async function main() {
const token = await getToken();
const organizationId = process.argv[2];
const formId = process.argv[3];
if (!formId || !organizationId) {
console.log("Please pass organizationId and formId.");
console.log("> yarn run get-intake-form <organizationId> <formId>");
} else {
try {
const result = await createIntakeFormResponse(
token,
organizationId,
formId,
fields
);
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.error(e);
}
}
}

main();
46 changes: 46 additions & 0 deletions src/intake-forms/get-intake-form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import fetch from "node-fetch";
import { readFile } from "fs/promises";
import { TOKEN_ENDPOINT, INTAKE_FORMS_API } from "./../api_urls.js";

// Gets the token by posting secrets.json to the token endpoint.
// See secrets-sample.json for an example.
async function getToken() {
const response = await fetch(TOKEN_ENDPOINT, {
method: "post",
body: await readFile("secrets.json"),
headers: { "content-type": "application/json" },
});
return await response.json();
}

async function getIntakeForm({ access_token }, organizationId, formId) {
const url = `${INTAKE_FORMS_API}/${organizationId}/intake-forms/${formId}`;
const response = await fetch(url, {
method: "get",
headers: {
authorization: `bearer ${access_token}`,
"content-type": "application/json",
},
});

return await response.json();
}

async function main() {
const token = await getToken();
const organizationId = process.argv[2];
const formId = process.argv[3];
if (!formId || !organizationId) {
console.log("Please pass organizationId and formId.");
console.log("> yarn run get-intake-form <organizationId> <formId>");
} else {
try {
const result = await getIntakeForm(token, organizationId, formId);
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.error(e);
}
}
}

main();
34 changes: 26 additions & 8 deletions src/vendors/set-attribute-value.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fetch from "node-fetch";
import { readFile } from "fs/promises";
import { TOKEN_ENDPOINT, VENDORS_API } from "./../api_urls.js";

/*
* Attributes can be updated by doing a PUT request to the legal-entities
Expand All @@ -10,9 +11,6 @@ import { readFile } from "fs/promises";
* that is represented by the token.
*/

const TOKEN_ENDPOINT = "https://api.vendorful.com/auth/v1/token";
const VENDORS_API = "https://api.vendorful.com/vendors/v1";

// Gets the token by posting secrets.json to the token endpoint.
// See secrets-sample.json for an example.
async function getToken() {
Expand All @@ -24,12 +22,22 @@ async function getToken() {
return await response.json();
}

async function setAttributeValue({ access_token }, organization_id, entity_id, attribute_id, value) {
async function setAttributeValue(
{ access_token },
organization_id,
entity_id,
attribute_id,
value
) {
console.log("accessToken", accessToken);
const url = `${VENDORS_API}/${organization_id}/legal-entities/${entity_id}/attributes/${attribute_id}`;
const response = await fetch(url, {
method: "put",
body: JSON.stringify({ value: value }),
headers: { authorization: `bearer ${access_token}`, "content-type": "application/json" },
headers: {
authorization: `bearer ${access_token}`,
"content-type": "application/json",
},
});
return await response.json();
}
Expand All @@ -41,11 +49,21 @@ async function main() {
const attribute_id = process.argv[4];
const value = process.argv[5];
if (!value) {
console.log("Please pass organization_id, entity_id, attribute_id, and value.");
console.log("> yarn run set-attribute-value <organization_id> <entity_id> <attribute_id> <value>");
console.log(
"Please pass organization_id, entity_id, attribute_id, and value."
);
console.log(
"> yarn run set-attribute-value <organization_id> <entity_id> <attribute_id> <value>"
);
} else {
try {
const result = await setAttributeValue(token, organization_id, entity_id, attribute_id, value);
const result = await setAttributeValue(
token,
organization_id,
entity_id,
attribute_id,
value
);
console.log(JSON.stringify(result, null, 2));
} catch (e) {
console.error(e);
Expand Down
9 changes: 4 additions & 5 deletions src/vendors/sync.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import fetch from "node-fetch";
import querystring from "querystring";
import { readFile, writeFile } from "fs/promises";
import { TOKEN_ENDPOINT, VENDORS_API } from "./../api_urls.js";

// Configuration would generally be stored in ENV or config files, but
// is inlined here for clarity.

// Update with target environment and api version
const TOKEN_ENDPOINT = "https://api.vendorful.com/auth/v1/token";
const VENDORS_API = "https://api.vendorful.com/vendors/v1";

// Update with your organization's Vendorful ID
const ORGANIZATION_ID = "acf6ce31-5abb-490e-b76f-15f299ca2853";

Expand All @@ -33,7 +30,9 @@ async function getEntities({ access_token }, updated_since) {
if (updated_since) params.updated_since = updated_since;

// Build url with query params
const url = `${VENDORS_API}/${ORGANIZATION_ID}/legal-entities?${querystring.encode(params)}`;
const url = `${VENDORS_API}/${ORGANIZATION_ID}/legal-entities?${querystring.encode(
params
)}`;

// Pass the access_token from the token in the authorization header
const response = await fetch(url, {
Expand Down