Skip to content

Schema Reference

A Schema is code which defines a component.

Schemas are defined as TypeScript functions that use the AssetBuilder API. This document augments the API documentation by laying out practical examples of each option, and briefly explaining what they do.

Schema Basics

Schemas define:

  • Component properties
  • Resource properties
  • Secrets the schema requires
  • Secrets the schema defines

The Builder Pattern

The Builder Pattern allows you to create an object, configure it, and eventually call build() on it to finalize and export the data. For example:

typescript
const asset = new AssetBuilder();

const keyName = new PropBuilder()
  .setName("KeyName")
  .setKind("string")
  .setWidget(new PropWidgetDefinitionBuilder().setKind("text").build())
  .build();
asset.addProp(keyName);

return asset.build();

There are 3 Builders in this snippet - the AssetBuilder, PropBuilder, and PropWidgetDefinitionBuilder. The pattern is always the same:

  1. Create the Builder.
  2. Configure the object.
  3. Call build().

The Asset Object

The AssetBuilder API is used to define the schema for a component. Schema definitions always begin by instantiating a new AssetBuilder, and end by returning the value of asset.build().

typescript
const asset = new AssetBuilder();

return asset.build();

Component Properties

Properties are added to a schema with the PropBuilder API and the addProp() function of the AssetBuilder.

Properties map to the underlying data structure of a schema, and specify things like field validations and influence the attribute panel's UI.

There are 6 kinds of properties, corresponding roughly to the standard JavaScript types:

  • array
  • boolean
  • integer
  • map
  • object
  • string

INFO

The difference between a map and an object is that maps take arbitrary key/value pairs, while objects have defined properties.

All properties have:

  • A Kind, which defines its fundamental data type, specified by setKind().
  • A Name, specified by setName().
  • An optional Validation Format, specified by setValidationFormat() which uses Joi to specify valid values.
  • An optional Default Value, specified by setDefaultValue().
  • An optional boolean field that hides the property from the attributes UI, specified with setHidden().
  • An optional Widget configuration, that determines how the field is presented in the attributes panel, specified with setWidget().

Boolean Properties

A boolean property named IsPublic with a default value of false.

typescript
const isPublicProp = new PropBuilder()
  .setName("IsPublic")
  .setKind("boolean")
  .setDefaultValue(false)
  .build();

String Properties

This example specifies a property named KeyName, whose value is a string:

typescript
const keyNameProp = new PropBuilder()
  .setKind("string")
  .setName("KeyName")
  .build();
asset.addProp(keyNameProp);

A string with options, that displays as a select box, with a default value:

typescript
const keyType = new PropBuilder()
  .setName("KeyType")
  .setKind("string")
  .setWidget(
    new PropWidgetDefinitionBuilder()
      .setKind("select")
      .addOption("rsa", "rsa")
      .addOption("ed25519", "ed25519")
      .build(),
  )
  .setDefaultValue("rsa")
  .build();
asset.addProp(keyType);

A string with a complex regular expression validation:

typescript
const cidrBlockProp = new PropBuilder()
  .setKind("string")
  .setName("CidrBlock")
  .setValidationFormat(
    Joi
      .string()
      .regex(
        /^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/)(1[6-9]|2[0-8])$/,
      )
      .messages({
        "string.pattern.base":
          "Must be a valid IPv4 CIDR with CIDR Blocks between /16 and /28",
      }),
  )
  .build();
asset.addProp(cidrBlockProp);

Integer Properties

A Throughput integer property:

typescript
const throughputProp = new PropBuilder()
  .setName("Throughput")
  .setKind("integer")
  .build();
asset.addProp(throughputProp);

Map Properties

A map named ResourceTags:

typescript
const resourceTags = new PropBuilder()
  .setKind("map")
  .setName("ResourceTags")
  .setEntry(
    new PropBuilder()
      .setKind("string")
      .setName("tag"),
  )
  .build();
asset.addProp(resourceTags);

Maps use the setEntry API to define the property kind of their value. Their keys are always string. The name of the Entry is displayed in the attribute panel, but is not present in the resulting data structure.

So this map would produce a data structure like:

typescript
{
  "van halen": "great band",
  "beyonce": "also great"
}

Any property kind is a valid map entry.

A more complex map of objects:

typescript
const resourceTags = new PropBuilder()
  .setKind("map")
  .setName("ResourceTags")
  .setEntry(
    new PropBuilder()
      .setName("Francis")
      .setKind("object")
      .addChild(
        new PropBuilder()
          .setName("Bacon")
          .setKind("boolean")
          .build(),
      ),
  )
  .build();
asset.addProp(resourceTags);

Would produce:

typescript
{
  "van halen": { "bacon": false },
  "beyonce": { "bacon": true },
}

Object Properties

An AdvancedConfiguration object, with 3 properties: InstanceProfileArn, EbsOptimized, and UserData:

typescript
const advancedConfigurationProp = new PropBuilder()
  .setName("AdvancedConfiguration")
  .setKind("object")
  .addChild(
    new PropBuilder()
      .setKind("string")
      .setName("InstanceProfileArn")
      .build(),
  )
  .addChild(
    new PropBuilder()
      .setKind("boolean")
      .setName("EbsOptimized")
      .build(),
  )
  .addChild(
    new PropBuilder()
      .setKind("string")
      .setName("UserData")
      .build(),
  )
  .build();
asset.addProp(advancedConfigurationProp);

Objects use the addChild interface to specify their properties. Any property kind can be a child property of an object.

This would produce an object that looks like this:

typescript
{
  InstanceProfileArn: "arn:...",
  EbsOptimized: false,
  UserData: "...",
}

Array Properties

A SecurityGroups array of strings:

typescript
const securityGroupsProp = new PropBuilder()
  .setKind("array")
  .setName("SecurityGroups")
  .setEntry(
    new PropBuilder()
      .setKind("string")
      .setName("SecurityGroup")
      .build(),
  )
  .build();
asset.addProp(securityGroupsProp);

Array's use the setEntry API to define the kind of their members. Any kind of property is valid as an array entry.

An array of objects:

typescript
const tagSpecificationsProp = new PropBuilder()
  .setName("TagSpecifications")
  .setKind("array")
  .setEntry(
    new PropBuilder()
      .setName("TagSpecificationsChild")
      .setKind("object")
      .addChild(
        new PropBuilder()
          .setName("Key")
          .setKind("string")
          .build(),
      )
      .addChild(
        new PropBuilder()
          .setName("Value")
          .setKind("string")
          .build(),
      )
      .build(),
  )
  .build();
asset.addProp(tagSpecificationsProp);

Widgets

Widgets define how properties are displayed. Each kind of property has a default widget, but it can be useful to alter the display on occasion (like the example above with KeyPair options.) Widgets are set with the PropWidgetDefinitionBuilder, and used through the setWidget() method on a PropBuilder.

Available widget types are:

  • array
  • checkbox
  • color
  • comboBox
  • header
  • map
  • select
  • text
  • textArea
  • codeEditor
  • password

The select widget and the comboBox widget both accept a list of options, set with the setOption() method on the builder.

WARNING

Important note - not all widget types are currently supported in the System Initiative web app.

Some widget types currently just display the same as text - this may change in the future!

Resource Properties

Resource Properties are used to extract information from Resources, and store them as hidden properties.

They are defined the same way as component properties, but with the setHidden(true) option set on the PropBuilder, and are added to the schema with addResourceProp() rather than addProp().

For example:

typescript
const instanceIdProp = new PropBuilder()
  .setName("InstanceId")
  .setKind("string")
  .setHidden(true)
  .build();
asset.addResourceProp(instanceIdProp);

Would extract the InstanceId from the resource information if it exists, and populate this property.

Secret Requirements

When a schema requires a secret, it is specified with the SecretPropBuilder API. It creates both a property that allows a secret of the given kind to be set.

They consist of:

  • The name of the secret prop
  • The secret kind, which corresponds to the name of its secret definition

Here is an example of an AWS Credential:

typescript
const credentialProp = new SecretPropBuilder()
  .setName("credential")
  .setSecretKind("AWS Credential")
  .build();
asset.addSecretProp(credentialProp);

This would allow the schema to accept any AWS Credential.

Secret Definitions

Schemas that define secrets should only define secrets. They use the SecretDefinitionBuilder to define themselves, and are added to the schema with the defineSecret() method. They consist of:

  • A name for the credential
  • Props that define the shape of the secret itself

For example, the Docker Hub Credential:

typescript
function main() {
  const credential = new SecretDefinitionBuilder()
    .setName("Docker Hub Credential")
    .addProp(
      new PropBuilder()
        .setName("Username")
        .setKind("string")
        .setWidget(
          new PropWidgetDefinitionBuilder()
            .setKind("password")
            .build(),
        ).build(),
    )
    .addProp(
      new PropBuilder()
        .setName("Password")
        .setKind("string")
        .setWidget(
          new PropWidgetDefinitionBuilder()
            .setKind("password")
            .build(),
        ).build(),
    )
    .build();
  return new AssetBuilder()
    .defineSecret(credential)
    .build();
}

Would allow users to add a Docker Hub Credential secret type, with two values, Username and Password.

Tips for Schema Creation

  1. Resist the temptation to abstract the resource you are modeling. Schemas in System Initiative work best when they are as close to 1:1 models of the upstream.
  2. Frequently the right set of properties for a component mirror what they need when they are created.