Building an Advanced Component That Handles Credentials

Overview

The goal of this quickstart is to examine the AWS S3 component to get you accustomed to developing more advanced components that may have multiple actions, handle credentials, or handle non-string binary data input fields.

AWS S3 component source code is referenced throughout this quickstart.

Prerequisites

You should read Building Your First Custom Component and Writing Custom Components prior to this quickstart to get a sense of project set up, and the action, input, and component objects that make up a component project.

Reusable Inputs

input definitions can be written in an action code block. For example, the Slack component is comprised of a single action, and inputs are defined within that action.

slack/src/index.ts
inputs: [
{
key: "webhookUrl",
label: "Webhook URL",
placeholder: "Slack Webhook URL",
type: "string",
required: true,
comments:
"Slack Webhook URL in the form `https://hooks.slack.com/services/FOO/BAR/BAZ`",
},
...
]

However, for sufficiently complex components, you will likely have multiple actions that rely on the same input. The S3 component, for example, has multiple actions that take the name of an S3 bucket as input. Defining the input field once, and referencing it by multiple actions results in cleaner, more concise code.

inputs.ts
import { input } from "@prismatic-io/spectral";
export const bucketInputField = input({
key: "bucket",
label: "Bucket Name",
placeholder: "Name of an S3 Bucket",
type: "string",
required: true,
comments: "Name of an S3 Bucket",
});
actions.ts
import { bucketInputField } from "./inputs";
import { action } from "@prismatic-io/spectral";
const deleteObject = action({
inputs: [awsRegionInputField, bucketInputField, keyInputField],
});
const getObject = action({
inputs: [awsRegionInputField, bucketInputField, keyInputField],
});
const listObjects = action({
inputs: [awsRegionInputField, bucketInputField, prefixInputField],
});

Reusable Helper Functions

S3 actions require an AWS S3 client, imported from the AWS SDK. Rather than wrapping the client creation logic into each action, authorization and client creation are placed in a distinct file, auth.ts, and referenced by the actions.

auth.ts
import AWS from "aws-sdk";
import { Credential } from "@prismatic-io/spectral";
export const createS3Client = (credential: Credential, region) => {
const accessKeyId = credential.fields.api_key;
const secretAccessKey = credential.fields.api_secret;
return new AWS.S3({
apiVersion: "2006-03-01",
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
region: region,
});
};
actions.ts
import { createS3Client } from "./auth";
const listObjects = action({
perform: async ({ credential }, { awsRegion, bucket, prefix }) => {
const s3 = createS3Client(credential, awsRegion);
},
});
const getObject = action({
perform: async ({ credential }, { awsRegion, bucket, objectKey }) => {
const s3 = createS3Client(credential, awsRegion);
},
});

Handling Credentials

Components can take a variety of credential types - see Authorization Methods for a list of types. To make AWS S3 API calls, an access_key_id and secret_access_key key pair are presented to AWS's API. An API Key / Secret, then, is a reasonable choice to hold AWS credentials.

Acceptable authorization methods are specified in the component object.

index.ts
export default component({
...
authorization: {
required: true,
methods: ["api_key_secret"],
},
});

The credential object is passed into an action's perform function as a parameter. For example,

actions.ts
const deleteObject = action({
perform: async ({ credential }, { awsRegion, bucket, objectKey }) => {
const s3 = createS3Client(credential, awsRegion);
},
});

Because the credential object is of type api_key_secret for this component, the credential object contains two fields: api_key and api_secret. Those fields are used in auth.ts to authenticate against the AWS API.

auth.ts
export const createS3Client = (credential: Credential, region) => {
const accessKeyId = credential.fields.api_key;
const secretAccessKey = credential.fields.api_secret;
return new AWS.S3({
apiVersion: "2006-03-01",
accessKeyId: accessKeyId,
secretAccessKey: secretAccessKey,
region: region,
});
};

Binary Data

For most components, passing text or JSON as inputs and outputs is sufficient. The Slack component, for example, will only ever post string messages.

The AWS S3 component, on the other hand, might fetch or write non-string data, like images, videos, or PDFs.

Binary Data as Outputs

The S3 getObject action reads a file (text or binary data) from an S3 bucket and returns its contents as an action output.

actions.ts
const getObject = action({
key: "getObject",
perform: async ({ credential }, { awsRegion, bucket, objectKey }) => {
const response = await s3.getObject(getObjectParameters).promise();
return {
data: response.Body as Buffer,
contentType: response.ContentType,
};
},
});
note

Note that in addition to the file's contents, the return contains contentType. That might be image/png, etc., depending on the type of the file.

Binary Data as Inputs

The S3 putObject action writes either plain text or a binary file to an S3 bucket. S3 extracts contentType from the file that is uploaded.

Our input is defined with type: "data" to indicate that it might contain binary data from a previous step.

inputs.ts
export const fileContentsInputField = input({
key: "fileContents",
label: "File Contents",
placeholder: "Output data from previous step, or a string, to write",
type: "data",
required: true,
comments: "Binary file data or a string",
});

If plain text is entered in the fileContents input field, a string is passed into the perform function. If a reference to binary data is passed in (for example, from an HTTP GET action's output), fileContents will be an object with .data and .contentType properties.

The S3 putObject action takes advantage of JavaScript's || syntax to write out fileContents.data if it exists, or it will fall back to writing out fileContents as a plaintext string.

actions.ts
perform: async (
{ credential },
{ awsRegion, bucket, fileContents, objectKey, tagging }
) => {
const s3 = createS3Client(credential, awsRegion);
const putParameters = {
Bucket: bucket,
Key: objectKey,
Body: fileContents.data || fileContents, // Write binary data or plain text
Tagging: tagging,
};
const response = await s3.putObject(putParameters).promise();
return {
data: response,
};
};
Last updated on