Skip to main content

Create a Model

Introduction

In CDK, a Model definition is a customized extension of one of the base Octo models.

A new model consists of a few core components,

  • Model is the base model extension.
  • Model Schema defines the structure and validation rules for model data.
  • Model Actions defines the actions that transforms the model into a set of resources.
info

In this tutorial we will define a custom region model for AWS from scratch.

A region in Octo is defined as a physical location where resources are deployed. In AWS, this translates to an AWS region like us-east-1 or eu-west-1.

For this example, we will assume that an entire AWS region is a single shard. Naturally, the total number of shards our users can then create is limited by the number of AWS regions. Further, we will restrict the total number of shards to be 2.

In real world this is rarely the case, and you will typically want to design your region to have multiple shards to maximize the total number of shards your users can create. But for demonstration purposes, this is a fair assumption.

Create a Model

To create an empty model, Octo provides you with a simple template.
Run this command from the root of your project,

npx @quadnix/octo-build create-model -n simple-region -t region --package @example

It should produce a directory structure like this,

src/modules/region/simple-region
└── models
└── region
├── actions
│ └── add-simple-region.model.action.ts
├── index.ts
├── simple-region.model.ts
└── simple-region.schema.ts

This model is currently empty, and we will write custom logic in these files.

Model Schema

A model schema defines the structure and validation rules for model data, and will always extend from the base model schema.
For region, the base model schema is RegionSchema.

  • RegionSchema defines regionId which is the unique identifier for the region.
  • Additionally, we define awsRegionAZs as a list of AWS availability zones in the region.
  • And, awsRegionId as the AWS region identifier.
  • We also validate our inputs using the built-in @Validate decorator.
simple-region.schema.ts
export class SimpleRegionSchema extends RegionSchema {
@Validate({
destruct: (value: SimpleRegionSchema['awsRegionAZs']): string[] => value,
options: { minLength: 1 },
})
awsRegionAZs = Schema<string[]>();

@Validate({ options: { minLength: 1 } })
awsRegionId = Schema<string>();
}

Model

A model class, like schema, extends from one of the base models.
For region, the base model is Region.

  • We redeclared the same properties as defined in the schema.
  • We then wrote a constructor to initialize the model data based on user inputs. We have restricted the awsRegionId to only 2 AWS regions, and we automatically populate the availability zones.
    In a production environment, you would want to allow more AWS regions, and set the AWS availability zones accordingly.
  • Calling super() ensures the base model initializes regionId. Here, regionId is same as awsRegionId.
  • We also marked our model with the @Model decorator.
simple-region.model.ts
export class SimpleRegion extends Region {
readonly awsRegionAZs: string[];

readonly awsRegionId: string;

override readonly regionId: string;

constructor (awsRegionId: ['us-east-1', 'us-west-2']) {
super(awsRegionId);

// E.g. for us-east-1, the AZs will be us-east-1a and us-east-1b.
this.awsRegionAZs = [`${awsRegionId}a`, `${awsRegionId}b`];

this.awsRegionId = awsRegionId;

this.regionId = awsRegionId;
}
}

Then we complete our model definition by implementing the required synth() and unSynth() methods to correctly serialize and deserialize the model.

simple-region.model.ts
override synth (): SimpleRegionSchema {
return {
awsRegionIds: [...this.awsRegionIds],
awsRegionId: this.awsRegionId,
regionId: this.regionId,
};
}

static override async unSynth (region: SimpleRegionSchema): Promise<SimpleRegion> {
return new SimpleRegion(region.awsRegionId);
}

Model Action

In the model action, we can transform our model into a set of resources. In this example we will reuse the SimpleVpc resource we created in the previous section.

The filter() method is used by Octo to determine which model actions to execute. When Octo attempts to add this model, this method is used to determine which model actions qualify for the given model and diff action.

add-simple-region.model.action.ts
filter(diff: Diff): boolean {
return (
diff.action === DiffAction.ADD &&
diff.node instanceof SimpleRegion &&
hasNodeName(diff.node, 'region') &&
diff.field === 'regionId'
);
}

The handle() method is where you can define the actual logic to generate resources.

  • actionInputs.inputs are module inputs provided by the users. Follow the link to learn more on how to create a module.
add-simple-region.model.action.ts
async handle(
diff: Diff<SimpleRegion>,
actionInputs: EnhancedModuleSchema<SimpleRegionModule>,
actionOutputs: ActionOutputs,
): Promise<ActionOutputs> {
const region = diff.node;

// Create a VPC.
const vpc = new SimpleVpc(`vpc-${region.regionId}`, {
awsAccountId: actionInputs.inputs.account.accountId,
awsAvailabilityZones: [...region.awsRegionAZs],
awsRegionId: region.awsRegionId,
CidrBlock: actionInputs.inputs.vpcCidrBlock,
InstanceTenancy: 'default',
});

actionOutputs[vpc.resourceId] = vpc;
return actionOutputs;
}

Export a Model

Once you have created a model, it needs to be exported for other parts of your module to consume it.

index.ts
import './actions/add-simple-region.model.action.js';

export { SimpleRegion } from './simple-region.model.js';

The index.ts file serves 2 main purpose,

  • It creates a single point of entry for your model, thus also controlling what you really want to expose vs internal implementation details.
  • It automatically wires up the model actions. Any time your model is imported anywhere, the model actions will be imported and the @Action decorator inside will be registered.
warning

If you miss importing any model actions in the index.ts, those actions won't get registered or triggered, since the @Action decorator never ran, and thus Octo will never know about this action.