Writing Custom Components

Overview

Prismatic is extensible and allows for developer users to develop their own custom components. Components that Prismatic users develop are proprietary to their organization, and are private.

Sample component code is referenced throughout this page.

Custom Component Library

Prismatic provides a NodeJS package, @prismatic-io/spectral, which provides TypeScript typing and some utility functions.

For information on Spectral's utility functions and types, see our custom component library docs.

Initializing a New Component

To initialize a new project, run prism components:init {{ COMPONENT NAME }}.

prism components:init format-name

Your component name must be comprised of alphanumeric characters, hyphens, and underscores, and start and end with alphanumeric characters. This will create a directory structure that looks like this:

format-name
├── assets
│   └── icon.png
├── jest.config.js
├── package.json
├── src
│   ├── index.test.ts
│   └── index.ts
├── tsconfig.json
└── webpack.config.js
  • assets/icon.png is the icon that will be displayed next to your component Square transparent PNGs at least 128 x 128 pixels in size look best, and will be scaled by the web app appropriately.
  • jest.config.js contains configuration for the Jest testing framework
  • package.json is a standard node package definition file
  • src/index.ts contains the code for your component. This file can be broken into multiple files as needed
  • src/index.test.ts contains tests for component actions defined in index.ts. See Unit Testing Custom Components
  • tsconfig.json contains configuration for TypeScript
  • webpack.config.js contains configuration for Webpack

After these files are created run cd {{ COMPONENT_NAME }} to enter the directory of your component, and then npm install or yarn install to install dependencies.

Writing Actions

A component is comprised of one or many actions. For example, the HTTP component contains actions to GET (httpGet), POST (httpPost), etc.

An action takes four arguments:

  1. A unique key name of the action, preferably in camelCase.
  2. Information on how the web app display the action
  3. A function to perform
  4. A series of input fields
action({
key: "myAction",
display: {
label: "Brief Description",
description: "Longer description to display in web app UI",
},
perform: functionToPerform(),
inputs: [inputFieldOne, inputFieldTwo],
});

Adding Inputs

Components can take inputs. Each input is comprised of a required key, label, and type and optional placeholder, default, comments, required and model.

Consider this example input:

const middleNameInputField = input({
key: "middleName",
label: "Middle Name",
placeholder: "Middle name of a person",
type: "string",
required: false,
default: "",
comments: "Leave blank if the user has no middle name",
});

This contributes to an input prompt that looks like this:

Note where the label and placeholder text appeared in the web app, and note that First Name and Last Name are required - indicated with a *, but Middle Name is not.

Input key names

The key specified above (in this case, "middleName") is accessible as an input for the perform function.

Writing perform functions

Each action contains one perform function, which is an async function with two parameters that may or may not have a return value. In this example firstName, middleName, and lastName, are input parameters for this perform function

const properFormatNameAction = action({
key: "properFormatName",
display: {
label: "Properly Format Name",
description: "Properly format a person's name (Last, First M.)",
},
perform: async (context, { firstName, middleName, lastName }) => {
if (middleName) {
return {
data: `${lastName}, ${firstName} ${middleName[0]}.`,
};
} else {
return { data: `${lastName}, ${firstName}` };
}
},
inputs: [firstNameInputField, middleNameInputField, lastNameInputField],
});

perform Function Parameters

The perform function takes two parameters, context and params, that can be destructured into their respective properties:

perform: async (context, params) => {},
// or
perform: async (
{ configVars, logger, credential },
{ paramName1, paramName2, ... }
) => {},

The context Parameter

The context parameter is an object that contains three attributes: configVars, logger and credential.

context.configVars

context.configVars is utilized only by the Custom Code Component, and is an object that contains key/value pairs of configuration variables that are accessible to the action. For example, if your integration has config variables named myEndpoint and username, you could access them like so:

Custom Code Block
module.exports = async (context, params) => {
const restEndpoint = `${context.configVars.myEndpoint}/?user=${context.configVars.username}`;
return restEndpoint;
};
note

context.configVars is only accessible to the custom code component, as that component takes no inputs. To pass config variables into other components, reference the config variables as inputs of your component.

context.logger

context.logger is a logging object and can be helpful to debug components.

perform: async ({ logger }, params) => {
logger.info("Things are going great");
logger.warn("Now less great...");
};

context.credential

context.credential is an object that stores information about any credentials tied to this action in the form

{
authorizationMethod:
"api_key_secret" | "basic" | "private_key" | "api_key" | "oauth2",
fields: {
FIELD_NAME: String,
FIELD_NAME: String,
},
}

The fields presented are dependent on what type of credential is passed in -- see Authorization Methods. If, for example, you use the basic authorization type, the credential payload might read

{
authorizationMethod: "basic",
fields: {
username: "MyUser",
password: "MyP@$$word",
},
}

The credential parameter can be used in your custom component like this:

perform: async ({ logger, credential }, params) => {
logger.log(`Your basic auth username is "${credential.fields.username}"`);
};

The params Parameter

The params parameter is an object that has attributes for each input field the action supports. For example, for the perform action defined above, params has params.firstName, params.middleName, and params.lastName.

firstName, middleName, and lastName are based off of the key of each input

const middleNameInputField = input({
key: "middleName",

The function is written with a destructured params parameter. It could be rewritten without being destructured.

perform: async (context, params) => {
if (params.middleName == "") {
return { data: `${params.lastName}, ${params.firstName}` };
} else {
return {
data: `${params.lastName}, ${params.firstName} ${params.middleName[0]}.`,
};
}
},

Component Outputs

In the example above, the function returns a string of the form Last, First M.. This return value of a custom component is accessible to subsequent steps by referencing this step's results:

Component outputs can take many forms. To return a simple string, number, boolean, list, or object your return block can read:

// return a string:
return {
data: "some string",
};
// return a number:
return {
data: 123.45,
};
// return a boolean:
return {
data: true,
};
// return a list:
return {
data: [1, 2, 3, 4, "a", "b"],
};
// return an object:
return {
data: {
key1: "value1",
key2: ["value2", 123],
},
};

Those values can be used as inputs in subsequent steps by referencing this step's results.

Outputting Binary Data

Some custom components will not output a number, string, boolean, list, or object, but will instead output an entire file (like an image, PDF, video, etc). For those custom components, the return value will contain a file Buffer as the data return, and a contentType key to indicate kind of file is being returned. See Mozilla's documentation for a list of common file MIME types.

For example, if your custom component returns a rendered PDF file, and the PDF contents are saved in a Buffer variable named pdfContents, the return block might look like this:

return {
data: pdfContents,
contentType: "application/pdf",
};

Setting Synchronous HTTP Status Codes

If you invoke your instances synchronously and would like to return an HTTP status code other than 200 - OK, you can configure the final step of your integration to be a custom component that returns any HTTP status code you want.

To return a status code other than 200, add the status code to the object you return from your custom component. For example,

return {
data: {
key1: "value1",
key2: ["value2", 123],
},
statusCode: 415,
};

If this custom component is the last step of an integration, then the integration will return an HTTP status code of 415 if invoked synchronously.

Exporting a Component

Component code contains a default export of component type. A component contains a key name, whether or not it's public, a version, some information about how the web app should display it, and a list of actions that the component contains. For the "proper and improper" names example component, the export can look like this

export default component({
key: "formatName",
public: true,
version,
display: {
label: "Format Name",
description: "Format a person's name given a first, middle, and last name",
icon: "icon.png",
},
actions: {
...improperFormatNameAction,
...properFormatNameAction,
},
});

Publishing a Component

Package a component with webpack by running npm run build or yarn build:

$ yarn build
yarn run v1.22.10
$ webpack
[webpack-cli] Compilation finished
asset index.js 37 KiB [compared for emit] (name: main)
asset icon.png 17.8 KiB [compared for emit] [from: assets/icon.png] [copied]
runtime modules 1.03 KiB 5 modules
modules by path ./node_modules/ 22.7 KiB
modules by path ./node_modules/date-fns/esm/ 14.6 KiB
modules by path ./node_modules/date-fns/esm/_lib/ 517 bytes
./node_modules/date-fns/esm/_lib/requiredArgs/index.js 221 bytes [built] [code generated]
./node_modules/date-fns/esm/_lib/toInteger/index.js 296 bytes [built] [code generated]
4 modules
modules by path ./node_modules/@prismatic-io/spectral/dist/*.js 4.05 KiB
./node_modules/@prismatic-io/spectral/dist/index.js 975 bytes [built] [code generated]
./node_modules/@prismatic-io/spectral/dist/util.js 3.02 KiB [built] [code generated]
./node_modules/@prismatic-io/spectral/dist/types.js 77 bytes [built] [code generated]
./node_modules/valid-url/index.js 3.99 KiB [built] [code generated]
./src/index.ts 1.5 KiB [built] [code generated]
./package.json 345 bytes [built] [code generated]
webpack 5.11.0 compiled successfully in 1344 ms
✨ Done in 2.60s.

This will create a dist/ directory containing your compiled JavaScript and icon image. Navigate to that directory, and use prism to publish your component.

$ cd dist/
$ prism components:publish
Format Name (1.0.0)
Format a person's name given a first, middle, and last name
Would you like to publish Format Name (1.0.0)? (y/N): y
Successfully submitted Format Name (1.0.0)! The publish should finish processing shortly.