Skip to main content

Building an Advanced Component That Handles Credentials

Overview#

The goal of this quickstart is to examine the Amazon 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.

Amazon 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
action({  /*...*/  inputs: {    webhookUrl: {      label: "Webhook URL",      placeholder: "Slack Webhook URL",      type: "string",      required: true,      comments:        "The Slack webhook URL. Instructions for generating a Slack webhook are available on the Slack component docs page.",      example: "https://hooks.slack.com/services/A/B/C",    },    /*...*/  },});

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 bucket = input({  label: "Bucket Name",  placeholder: "Name of an S3 Bucket",  type: "string",  required: true,  comments:    "An AWS S3 'bucket' is a container where files are stored. You can create a bucket from within the AWS console. Bucket names contain only letters, numbers, and dashes.",  example: "my-s3-bucket-abc123",});
actions.ts
import { bucketInputField } from "./inputs";import { action } from "@prismatic-io/spectral";
const deleteObject = action({  inputs: { awsRegion, bucket, objectKey },});
const getObject = action({  inputs: { awsRegion, bucket, objectKey },});
const listObjects = action({  inputs: { awsRegion, bucket, prefix },});

Reusable Helper Functions#

S3 actions require an Amazon 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 = async (  credential: Credential,  region: string) => {  if (credential.authorizationMethod !== "api_key_secret") {    throw new Error(      `Unsupported authorization method ${credential.authorizationMethod}.`    );  }
  const credentials = {    accessKeyId: credential.fields.api_key,    secretAccessKey: credential.fields.api_secret,    region,  };
  // Verify credentials are valid with STS.getCallerIdentity()  const sts = new AWS.STS(credentials);  try {    await sts.getCallerIdentity({}).promise();  } catch (err) {    throw new Error(      `Invalid AWS Credentials have been configured. This is sometimes caused by trailing spaces in AWS keys, missing characters from a copy/paste, etc. Original AWS error message: ${err.message}`    );  }
  return new AWS.S3(credentials);};
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 Amazon 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: string) => {  /*...*/  const credentials = {    accessKeyId: credential.fields.api_key,    secretAccessKey: credential.fields.api_secret,    region,  };  return new AWS.S3(credentials);};

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 Amazon 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 fileContents = input({  label: "File Contents",  placeholder: "Output data from previous step, or a string, to write",  type: "data",  required: true,  comments:    "The contents to write to a file. This can be a string of text, it can be binary data (like an image or PDF) that was generated in a previous step.",  example: "My File Contents",});

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 Spectral's util.types.toData() function to turn fileContents into a type that the AWS SDK expects.

actions.ts
const putObject = action({  perform: async (    { credential },    { awsRegion, bucket, fileContents, objectKey, tagging }  ) => {    const s3 = await createS3Client(credential, util.types.toString(awsRegion));    const { data, contentType } = util.types.toData(fileContents);    const tags = querystring.encode(      (tagging || []).reduce(        (acc, { key, value }) => ({ ...acc, [key]: value }),        {}      )    );    const putParameters: S3.PutObjectRequest = {      Bucket: util.types.toString(bucket),      Key: util.types.toString(objectKey),      Body: data,      ContentType: contentType,      Tagging: tags,    };    const response = await s3.putObject(putParameters).promise();    return {      data: response,    };  },  inputs: {    awsRegion,    bucket,    fileContents,    objectKey,    tagging,  },});