@smartsoftware/dexterity
v1.0.50
Published
Smart Software Solutions developed framework for AWS centric serverless applications.
Downloads
9
Readme
Smart Dexterity
License to Use
Smart Dexterity is an AWS-centric serverless framework designed, developed, and maintained by Smart Software Solutions, Inc. It is not intended for general public consumption, but is offered to clients as a "right to use in perpetuity" for the projects in which they are implemented by Smart Software Solutions, Inc. This "right to use" is granted by a contractual agreement between the client and Smart Software Solutions, Inc and may be voided per terms of that agreement. The purpose of this distribution is to ensure indefinite and stable execution of delivered projects by Smart Software Solutions.
Any Code, Documentation, or related assets should not be modified, replicated, or redistributed into any other systems or to any other parties other than those explicitly granted by Smart Software Solutions, Inc. Inquiries of this nature can be directed to Jesse Bethke or Matthew Smart of Smart Software Solutions, Inc.
The distributed code is purposefully uglified, minified, and mangled to protect Smart Software Solutions's interests in it's intellectual property as well as the security of projects consuming it. Any attempt to decompile or reverse engineer the code is prohibited and a violation of Smart Software Solutions rights to the software.
Consumption of Libraries
Smart Dexterity is intended to be added as an NPM dependency for a Dexterity Workspace Project. A Workspace project is typically initialized from a clone of the smart-dexterity-template repository. Please reference documentation specific to that project.
You may reference the git repository, jbethke/smart-dexterity, for a latest development build or the npm package, @smartsoftware/dexterity
Smart Dexterity is intended to be used with Typescript but can be consumed by native Javascript. If using Typescript, the library does emit types with documentation.
Configuration
Smart Dexterity relies on the node library, Config and hosts it's configuration files in a 'config' folder.
Accessing Config Properties
Within your module, import DexConfig
from @smartsoftware/dexterity
and then use it in the same fashion that you might the Config library.
Example:
import { DexConfig} from '@smartsoftare/dexterity';
let domain = DexConfig.get('CloudFormation.domain.baseDomain');
Properties are considered immutable, meaning they should not be altered at run time.
Environment Dependent Config Properties
Like the main Config library, you can override properties for specific environment by creating a json file for that environment in the config folder.
Example stage.json
sets the resource prefix to 'stage'
{
"CloudFormation": {
"resourcePrefix": "stage"
}
}
Purpose and Usage of Config Properties
The config properties defined below are used to generate the CloudFormation templates for automated deployment, upgrades, and migrations. They may also be referenced at runtime to automatically locate resources at AWS.
Generally, all properties are considered to be required unless a default is explicitly documented.
As a side note, you can of course any any additional configuration properties that may be useful to your application. They shouldn't interfere with Dexterity's functionality.
CloudFormation
This includes details for generating the CloudFormation template for deployment. Additionally, when the angular application builds, it'll generate a config asset for automatically referencing the api endpoint and Cognito identity pool.
Properties
- accountId: Your AWS account id. Used for generating ARN references to resources.
- resourcePrefx: All resources created at AWS will be prefix in the pattern
{resourcePrefix}-{appTag}-{resourceName}
. Typically, the resourcePrefix would be your target environment, such as 'stage'. - appTag: All resources created at AWS will be prefix in the pattern
{resourcePrefix}-{appTag}-{resourceName}
. Typically, the appTag would be your project's short alias, like "dex" - region: The AWS region to use for deployment. Keep in mind that not all AWS resources are available in all regions. For this reason, we recommend "us-west-2"
- credentials.aws_access_key_id: Your IAM access key with privileges to create resources at AWS
- credentials.aws_secret_access_key: Your IAM access key with privileges to create resources at AWS
- domain.baseDomain: The root level domain that you wish for Route 53 entries to be created at. Entries are generally created as subdomains, for example
site-stage-app.basedomain.com
- domain.certificateArn: All endpoints are created to support SSL but must have a wildcard certificate created for that domain. Create one in the us-east-1 region, and then reference the ARN here.
Application
Properties under this segment are intended for Application runtime behavior and deployment configuration.
Properties
- authentication: Object with sub-properties
- attachTriggers: Boolean. default false. If True, then when the CognitoPool is created, trigger events will be attached to direct to the API stack's function.
- aliasAttributes: string[]. Attributes used as a username alias. Commonly: ["email", "phone"]
- autoVerifiedAttributes: string[]. An attributes that should automatically be marked as verified during registration. Commonly: ["email", "phone"]
- passwordPolicy: Object with sub-properties
- MinimumLength: Number. default
8
. Minimum length of password - RequireLowercase: boolean. default
false
. Whether or not to require that the password includes a lower-case character - RequireUppercase: boolean. default
false
. Whether or not to require that the password includes a upper-case character - RequireNumbers: boolean. default
false
. Whether or not to require that the password includes a numerical character - RequireSymbols: boolean. default
false
. Whether or not to require that the password includes a symbol character
- MinimumLength: Number. default
- encryption: Object with sub-properties
- algorithm: default:
aes-256-ctr
. Algorithm to use when encrypting passwords. - password: Key to use with algorithm when encrypting. If omitted, dexterity will internally determine a key
- encryptApiTraffic: boolean. default
false
Default behavior for api gateway traffic.
- algorithm: default:
- groups: array of group definitions
- group.groupname string. name of a group, like "Admin"
- group.description string. optional. description of the group
- providers: object with sub-properties used to define third-party identity providers, such as Facebook. Look up documentation specific to your provider.
- email: object with sub-properties
- defaultFromAddress: string. an email to use as the from address for messages generated by cognito or when using SES and not explicitly defining a from address
- stacks: an array of stack definitions. See Dexterity Stacks below.
- nodeVersion: Version of Node for which the application is compiled against. It must be a version compatible with both Lambda and Dexterity. Currently 8.10 and 10.x
Dexterity Stacks
A Smart Dexterity Workspace is composed of 'stacks'. A stack is a collection of resources for a project and is some what analogous to an AWS CloudFormation nested stack.
Generally, resources should be grouped into their stacks by type. For example, a Dynamo stack includes DynamoTable references and models.
Stacks are added by placing them under the stacks
property in the config. Each stack has the required type
property that must be one of the types defined below. Each stack type then may have additional required or optional parameters.
Example:
{
"stacks": [
{
"type": "Dynamo"
}
]
}
Consequently, you can have different stacks per target environment, varying config properties per stack per environment, or multiple stacks of the same type in an environment.
Cognito Stack
A Cognito stack adds a Cognito user pool and identity pool and links them. If a cognito stack exists, it'll become an optional auth mapping for any API Gateways also added to the project.
Configuration for Cognito stacks
Cognito stacks themselves have no additional configuration parameters at this time, but instead pull their configuration from the Application.authentication
namespace of the config.
Dynamo Stack
A dynamo stack will include a collection of models that are then used to automatically generate dynamo tables.
Configuration for Dynamo Stacks
- type: exactly "Dynamo"
- package: The name of the npm package for the sub-project that hosts this stack. This may be used for inter-project references and namespacing.
- localPath: The name of the folder relative to the root of the workspace where this stack's code is located. Default is "Dynamo"
Table specific definitions are part of the dynamo model definition. Please reference that documentation.
Api Stack
An API Stack is a combination of the API Gateway and a Lambda function that is used as a proxy. Services defined in the API Stack then become endpoints in the API Gateway.
By default, after deployment the domain of your API Gateway will be:
api-{resourcePrefix}-{appTag}.{baseDomain}
However, you can manually alias this path in Route 53. We recommend this as it makes it easier in the future to spin up secondary environments and reroute traffic between them.
Configuration for API Stacks
- type: exactly "Api"
- package: The name of the npm package for the sub-project that hosts this stack. This may be used for inter-project references and namespacing.
- localPath: The name of the folder relative to the root of the workspace where this stack's code is located. Default is "Lambda"
Site Stack
A site stack is a combination of a CloudFront endpoint and an s3 bucket path mapping. Each stack may map to one bucket but have multiple endpoint mappings. You can have multiple site stacks for multiple buckets.
When pairing with @smartsoftware/dexterity-angular, the angular build distribution is commonly placed in an s3 bucket under the /site
path.
The result of those default settings is an endpoint like:
site-{resourcePrefix}-{appTag}.{baseDomain}
The prefix is derived from the relativeBucketPath
property
Configuration for Site Stacks
- type: exactly "Dynamo"
- domains: An array of CloudFront endpoints to be created.
Domain endpoint properties
- localPath: The relative path from the workspace root to the folder hosting the files to be uploaded to S3 during deployment. If the folder contains a
dist
folder, then only the contents of that dist folder will be uploaded. This is typical for Angular projects. - relativeBucketPath: The path to be created in the dexterity workspace's s3 bucket for hosting these files.
- accessPrivilege: {"READ" | "WRITE"} This sets the privileges of the lambda execution role for the application. Typically a "Site" path will be read only and a "Content" path will permit writing. This prevents the application from writing to itself or altering execution code. In all cases, the bucket's public read is disabled and only authorized by an explicitly CloudFront IAM access role that is automatically created.
ElasticSearch Stack
This creates an ElasticSearch cluster. At this time, customizations are limited and must be manually done post-deployment. However, everything to start using immediately are handled automatically. This includes creating explicit grant permissions in the Lambda Execution Role and an index for your application's workspace. ElasticDynamoModels
with a search index decorator will also dynamically create elastic document models with appropriate mappings.
Configuration for ElasticSearch Stacks
- type: exactly "ElasticSearch"
Queue Stack
A queue stack simply creates SQS queue. As part of a Queue stack, you would typically create a class that extends DexQueue to facilitate interface with the Queue. You may also add Triggers to react to events from the Queue.
Each SQS queue is automatically created with the {resourcePrefix}-{appTag}-{QueueName}
pattern with appropriate access rules added to the Lambda Execution Role. However, when referencing the queue within your Lambda stack, you should either use the QueueName or the alias class if one is created.
Configuration for Queue Stacks
- type: exactly "Queue"
- queues: An array of Queue definitions. Each definition simply has a
QueueName
property, but may be extended in the future.
Dynamo Models
A Dexterity workspace project may optionally have one or more Dynamo stacks. A dynamo stack is a collection of related Dynamo tables, effectively representing a 'database'. Each table should be defined using a dexterity model.
There are different types of models:
- BaseModel: An abstract interface by which all Dexterity models implement or inherit. You may create BaseModels for any purpose, but they wont necessarily become tables.
- DynamoModel: A model explicitly intended to create a Dynamo table.
- ElasticDynamoModel: An extension of the Dynamo model, but with additional features for automated casting to an ElasticSearch index.
You can create your own model types or import models from other Dexterity libraries. The core library only provides models for Dynamo and ElasticSearch.
Creating a Model
A model class will extend one of the model types above, which are imported from the @smartsoftware/dexterity
library.
To create a Dynamo table from your class, implement a Table
or TableName
decorator. The TableName
decorator takes a string for the table name. If you need to set additional properties, Table
will take an object with properties matching the TableDefinition
interface.
Example:
import {
DynamoModel
TableName,
FieldName
} from '@smartsoftware/dexterity';
@TableName('Article')
export class ArticleModel extends DynamoModel {
@FieldName('title')
public title : string = "Unnamed Article";
constructor(jsonObject?: {}) {
super();
this.mapData(jsonObject);
}
}
Like the table decorators, each attribute that you wish to become a field in the dynamo table must be decorated with either the Field
or FieldName
. Once again, FieldName takes a string for the name of the field in dynamo. Field accepts an object matching the FieldDefinition
interface.
For definition interfaces, please rely on the types definitions emitted from the Dexterity library.
Do take special note of the constructor in the example above. This MUST be present. It permits casting and instantiation of the strongly typed model from untyped data received either from Dynamo or the API Gateway. When data is passing either direction, it is cast into this model. This permits you to enforce validation or translation of data.
If using get/set accessors, place the Field definition above the get accessor. Any attributes that you add that do not have field definitions will not appear in the output cast when sent to Dynamo.
Secondary Indexes for Dynamo Models
Dynamo is a nosql database, meaning it does not have support for foreign keys, but instead may use secondary indexes with projections of the data set.
Make sure you understand the behavior of Dynamo table queries and the impact of creating secondary indexes. If improperly defined, they can negatively impact performance while increasing storage and hosting costs.
These can be defined as part of the TableDefinition
with an optional GlobalSecondaryIndexes
property that takes an array of GlobalSecondaryIndexDefinition
s. Please reference these type definitions for additional documentation, but an example is provided below.
import {
DynamoModel
TableName,
FieldName
} from '@smartsoftware/dexterity';
@Table(
Name: 'Article'
GlobalSecondaryIndexes: [
Name: "RouteIndex",
PartitionKey: "route",
sortKey: "updatedAt"
]
)
export class ArticleModel extends DynamoModel {
@FieldName('title')
public title : string = "Unnamed Article";
@FieldName('route')
public route : string;
constructor(jsonObject?: {}) {
super();
this.mapData(jsonObject);
}
}
Advanced Field Definitions
In most cases, you'll simply want to use the FieldName
decorator to define a field. However, you may optionally use the Field
decorator with a FieldDefinition
object.
Example:
[...]
export class ArticleModel extends DynamoModel {
@Field({
name: "content",
description: "HTML content representing the body of this article.",
type: AttributeType.String
})
public content : ArticleContent;
}
If you provide a description, it will appear in generated schema definitions, including any API Gateway Swagger documentation.
The type can be automatically inferred from the typescript typing, but if you are using a custom type for some reason, you'll want to map it to a compatible Dynamo type. In the example above, ArticleContent.toString()
will be leveraged in casting the complex type for insertion into Dynamo.
Elastic Dynamo Models
Elastic Dynamo Models are an extension of the base Dynamo Model, except with additional functionality for ElasticSearch.
Whenever an ElasticDynamoModel is updated, the data is stored in both Dynamo and ElasticSearch simultaneously. When fetching, Dexterity automatically determines which source to pull from based on the nature of your request. In both cases, the model is cast into the same class and can be interacted with uniformly down-stream.
During project deployment, ElasticSearch model mappings will be created automatically for each ElasticDynamoModel.
To upgrade a Dynamo model to an ElasticDynamoModel, simply extend that class and add a SearchIndex
decorator.
Example:
import {
ElasticDynamoModel
TableName,
SearchIndex
} from '@smartsoftware/dexterity';
@TableName('Article')
@SearchIndex({
name: "Article"
})
export class ArticleModel extends ElasticDynamoModel {
}
Reference the SearchIndexDefinition
type definition for documentation.
We generally recommend that you are consistent in your naming between the model name, the table name, and the index name. However, it's not strictly required.
Inherited Properties
All DynamoModels automatically receive several properties that are also automatically set by interaction with them.
- createdAt: Date. The datetime that the model was created.
- updatedAt: Date. The datetime that the model was last modified.
- deletedAt: Date. The datetime that the model was deleted. If
null
, then the model is not deleted - uuid: string. A dynamically generated guid to use as the primary key in referencing this model. You may override with your own schema.
Extending Dexterity Models
The base model class and inherited classes have some built-in functions and accessors that automated much of the interaction with dexterity models. However, for special use cases, you may need to override or overload these routines.
mapData
public mapData(jsonObject?: any) : DynamoModel {
super.mapData(jsonObject);
return this;
}
Takes an incoming data object and then recursively maps each of the key/value pairs into the instance of the model. The mapping observes the model's typed definitions to force castings. For example, a date string into a Date
object.
get modelData
public get modelData() : any { }
This casts the model object into a data object appropriate for insertion into Dynamo. It will include only field mappings and translate them into appropriate data types.
get indexData
public get indexData() : any { }
This casts the model object into a data object appropriate for insertion into json-based indexing services, like ElasticSearch. It will include only field mappings and translate them into appropriate data types.
toPageContent
public toPageContent() : PageContent {
let content : PageContent = new PageContent();
return content;
}
If using the Page Cache functionality from @smartsoftware/dexterity-services, then you may want to cast models into PageContent
objects. Please reference that library for further documentation.
Change Log
- 1.0.44 - Added support for SQS queries
- 1.0.45 - Added support for 'triggers'
- 1.0.46 - Added support for aes encryption of payloads in transit
- 1.0.47 - Added improved SNS support for push notifications to iOS devices
- 1.0.48 - Added support for Node 10x on Lambda
- 1.0.49 - Moved the Sharp dependency from smart-dexterity to smart-dexterity-media
- 1.0.50 - Patch for deprecation of UnusedAccountValidityDays from Cognito UserPool configuration