using System; using System.Collections.Generic; using System.Linq; using System.Text; using Amazon.DynamoDBv2.Model; namespace Amazon.PowerShell.Cmdlets.DDB.Model { /// /// Model class carrying details of an Amazon DynamoDB table schema under /// construction for use with the New-DDBTable cmdlet. This class is accepted /// as pipeline input by the schema builder cmdlets (Write-DDBAttributeSchema, /// Write-DDBKeySchema and Write-DDBIndexSchema). /// public class TableSchema { private readonly string[] _projectionTypes = new string[] { "KEYS_ONLY", "INCLUDE", "ALL" }; #region Construction private TableSchema() { } /// /// Constructs a new table schema from a deep copy of the suppled object. /// Note that ICloneable is not supported on the coreclr platform, so we /// chose not to make the type derive from ICloneable but retained the /// Clone method. /// /// private TableSchema(TableSchema source) { CloneAttributeSchema(source.AttributeSchema); CloneKeySchema(source.KeySchema); CloneLocalSecondaryIndexSchema(source.LocalSecondaryIndexSchema); CloneGlobalSecondaryIndexSchema(source.GlobalSecondaryIndexSchema); } internal static TableSchema From(TableSchema source) { return source == null ? new TableSchema() : new TableSchema(source); } #endregion public object Clone() { return From(this); } #region Properties List _attributeSchema = new List(); public List AttributeSchema { get { return _attributeSchema; } } internal bool HasDefinedAttributes { get { return _attributeSchema.Count > 0; } } List _keySchema = new List(); public List KeySchema { get { return _keySchema; } } internal bool HasDefinedKeys { get { return _keySchema.Count > 0; } } List _localSecondaryIndexSchema = new List(); public List LocalSecondaryIndexSchema { get { return _localSecondaryIndexSchema; } } internal bool HasDefinedLocalSecondaryIndices { get { return _localSecondaryIndexSchema.Count > 0; } } List _globalSecondaryIndexSchema = new List(); public List GlobalSecondaryIndexSchema { get { return _globalSecondaryIndexSchema; } } internal bool HasDefinedGlobalSecondaryIndices { get { return _globalSecondaryIndexSchema.Count > 0; } } #endregion #region Attribute Schema Operations private void CloneAttributeSchema(IEnumerable sourceSchema) { AttributeSchema.AddRange(sourceSchema.Select(attr => new AttributeDefinition { AttributeName = new string(attr.AttributeName.ToCharArray()), AttributeType = new DynamoDBv2.ScalarAttributeType(attr.AttributeType) })); } internal void AddAttribute(string attributeName, string attributeType) { if (string.IsNullOrEmpty(attributeType)) throw new ArgumentException(string.Format("Attribute type must be specified", "AttributeType")); var existingAttr = AttributeSchema.FirstOrDefault(attr => attr.AttributeName.Equals(attributeName, StringComparison.OrdinalIgnoreCase)); if (existingAttr != null) { if (!attributeType.Equals(existingAttr.AttributeType, StringComparison.OrdinalIgnoreCase)) throw new ArgumentException(string.Format("An attribute with name {0} had been defined previously but conflicting type ({1} vs {2}) has already been defined.", attributeName, existingAttr.AttributeType, attributeType), "AttributeName"); return; } AttributeSchema.Add(new AttributeDefinition { AttributeName = attributeName, AttributeType = attributeType.ToUpper() }); } #endregion #region Key Schema Operations private void CloneKeySchema(IEnumerable sourceSchema) { KeySchema.AddRange(sourceSchema.Select(key => new KeySchemaElement { AttributeName = new string(key.AttributeName.ToCharArray()), KeyType = new DynamoDBv2.KeyType(key.KeyType) })); } internal void AddKey(string keyName, string keyType, string dataType) { var keyElement = KeySchema.FirstOrDefault(key => key.AttributeName.Equals(keyName, StringComparison.OrdinalIgnoreCase)); if (keyElement != null) throw new ArgumentException(string.Format("A key with name '{0}' has already been defined in the schema.", keyName), "KeyName"); // if the key doesn't already exist as an attribute, add it var attribute = AttributeSchema.FirstOrDefault(a => a.AttributeName.Equals(keyName, StringComparison.OrdinalIgnoreCase)); if (attribute == null) { if (string.IsNullOrEmpty(dataType)) throw new ArgumentException("An attribute for the key was not found in the supplied schema. The data type is needed before it can be added automatically.", "DataType"); AttributeSchema.Add(new AttributeDefinition { AttributeName = keyName, AttributeType = dataType }); } keyElement = new KeySchemaElement { AttributeName = keyName, KeyType = keyType }; // allow for user possibly defining keys in any order; DDB requires the primary hash key to be first // and there may be multiple hash keys allowed in future. if (!HasDefinedKeys || keyType.Equals(Amazon.DynamoDBv2.KeyType.RANGE, StringComparison.OrdinalIgnoreCase)) KeySchema.Add(keyElement); else if (KeySchema[0].KeyType.Equals(Amazon.DynamoDBv2.KeyType.HASH)) KeySchema.Add(keyElement); else KeySchema.Insert(0, keyElement); } #endregion #region Local Secondary Index Schema Operations private void CloneLocalSecondaryIndexSchema(IEnumerable sourceSchema) { foreach (var sourceIndex in sourceSchema) { var clonedIndex = new LocalSecondaryIndex { IndexName = new string(sourceIndex.IndexName.ToCharArray()), Projection = null, KeySchema = new List() }; foreach (var sourceKey in sourceIndex.KeySchema) { var clonedKey = new KeySchemaElement { AttributeName = new string(sourceKey.AttributeName.ToCharArray()), KeyType = new DynamoDBv2.KeyType(sourceKey.KeyType) }; clonedIndex.KeySchema.Add(clonedKey); } if (sourceIndex.Projection != null) { clonedIndex.Projection = new Projection { ProjectionType = new DynamoDBv2.ProjectionType(sourceIndex.Projection.ProjectionType), NonKeyAttributes = new List() }; foreach (var nonKeyAttr in sourceIndex.Projection.NonKeyAttributes) { clonedIndex.Projection.NonKeyAttributes.Add(nonKeyAttr); } } LocalSecondaryIndexSchema.Add(clonedIndex); } } /// /// Adds a new local secondary index or updates an index if it has been defined already /// /// /// /// /// /// internal void SetLocalSecondaryIndex(string indexName, string rangeKeyName, string rangeKeyDataType, string projectionType = null, string[] nonKeyAttributes = null) { // to add additional range keys, the user invokes this cmdlet multiple times in the // pipeline bool creatingNewIndex = false; var lsi = LocalSecondaryIndexSchema.FirstOrDefault(l => l.IndexName.Equals(indexName, StringComparison.OrdinalIgnoreCase)); if (lsi == null) { creatingNewIndex = true; lsi = new LocalSecondaryIndex { IndexName = indexName, KeySchema = new List() }; } if (AttributeSchema.FirstOrDefault(a => a.AttributeName.Equals(rangeKeyName, StringComparison.Ordinal)) == null) { // could validate that data type matches here AttributeSchema.Add(new AttributeDefinition { AttributeName = rangeKeyName, AttributeType = rangeKeyDataType }); } lsi.KeySchema.Add(new KeySchemaElement { AttributeName = rangeKeyName, KeyType = Amazon.DynamoDBv2.KeyType.RANGE }); if (!string.IsNullOrEmpty(projectionType)) { lsi.Projection = new Projection(); if (_projectionTypes.Contains(projectionType, StringComparer.OrdinalIgnoreCase)) { lsi.Projection.ProjectionType = projectionType.ToUpper(); if (nonKeyAttributes != null && nonKeyAttributes.Length > 0) { lsi.Projection.NonKeyAttributes.AddRange(nonKeyAttributes); } } else throw new ArgumentException(string.Format("Invalid ProjectionType '{0}'; allowed values: {1}.", projectionType, string.Join(", ", _projectionTypes)), "ProjectionType"); } if (creatingNewIndex) LocalSecondaryIndexSchema.Add(lsi); } #endregion #region Global Secondary Index Schema Operations private void CloneGlobalSecondaryIndexSchema(IEnumerable sourceSchema) { foreach (var sourceIndex in sourceSchema) { var clonedIndex = new GlobalSecondaryIndex { IndexName = new string(sourceIndex.IndexName.ToCharArray()), Projection = null, KeySchema = new List() }; foreach (var sourceKey in sourceIndex.KeySchema) { var clonedKey = new KeySchemaElement { AttributeName = new string(sourceKey.AttributeName.ToCharArray()), KeyType = new DynamoDBv2.KeyType(sourceKey.KeyType) }; clonedIndex.KeySchema.Add(clonedKey); } if (sourceIndex.Projection != null) { clonedIndex.Projection = new Projection { ProjectionType = new DynamoDBv2.ProjectionType(sourceIndex.Projection.ProjectionType), NonKeyAttributes = new List() }; foreach (var nonKeyAttr in sourceIndex.Projection.NonKeyAttributes) { clonedIndex.Projection.NonKeyAttributes.Add(nonKeyAttr); } } clonedIndex.ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = sourceIndex.ProvisionedThroughput.ReadCapacityUnits, WriteCapacityUnits = sourceIndex.ProvisionedThroughput.WriteCapacityUnits }; GlobalSecondaryIndexSchema.Add(clonedIndex); } } /// /// Adds a new global secondary index or updates an index if it has been defined already. /// /// /// /// /// /// /// /// /// /// internal void SetGlobalSecondaryIndex(string indexName, string hashKeyName, string hashKeyDataType, string rangeKeyName, string rangeKeyDataType, Int64 readCapacityUnits, Int64 writeCapacityUnits, string projectionType = null, string[] nonKeyAttributes = null) { // to add additional range keys, the user invokes this cmdlet multiple times in the // pipeline bool creatingNewIndex = false; var gsi = GlobalSecondaryIndexSchema.FirstOrDefault(g => g.IndexName.Equals(indexName, StringComparison.OrdinalIgnoreCase)); if (gsi == null) { creatingNewIndex = true; gsi = new GlobalSecondaryIndex { IndexName = indexName, KeySchema = new List() }; } // for a GSI, an additional hash key can be defined that does not have to match that used by the table if (!string.IsNullOrEmpty(hashKeyName)) { if (AttributeSchema.FirstOrDefault(a => a.AttributeName.Equals(hashKeyName, StringComparison.Ordinal)) == null) { // could validate that data type matches here AttributeSchema.Add(new AttributeDefinition { AttributeName = hashKeyName, AttributeType = hashKeyDataType }); } gsi.KeySchema.Add(new KeySchemaElement { AttributeName = hashKeyName, KeyType = Amazon.DynamoDBv2.KeyType.HASH }); } // for global indexes, range key is optional (assuming a hash key has been specified); for local indexes the range key is // mandatory if (!string.IsNullOrEmpty(rangeKeyName)) { if (AttributeSchema.FirstOrDefault(a => a.AttributeName.Equals(rangeKeyName, StringComparison.Ordinal)) == null) { // could validate that data type matches here AttributeSchema.Add(new AttributeDefinition { AttributeName = rangeKeyName, AttributeType = rangeKeyDataType }); } gsi.KeySchema.Add(new KeySchemaElement { AttributeName = rangeKeyName, KeyType = Amazon.DynamoDBv2.KeyType.RANGE }); } gsi.ProvisionedThroughput = new ProvisionedThroughput { ReadCapacityUnits = readCapacityUnits, WriteCapacityUnits = writeCapacityUnits }; if (!string.IsNullOrEmpty(projectionType)) { gsi.Projection = new Projection(); if (_projectionTypes.Contains(projectionType, StringComparer.OrdinalIgnoreCase)) { gsi.Projection.ProjectionType = projectionType.ToUpper(); if (nonKeyAttributes != null && nonKeyAttributes.Length > 0) { gsi.Projection.NonKeyAttributes.AddRange(nonKeyAttributes); } } else throw new ArgumentException(string.Format("Invalid ProjectionType '{0}'; allowed values: {1}.", projectionType, string.Join(", ", _projectionTypes)), "ProjectionType"); } if (creatingNewIndex) GlobalSecondaryIndexSchema.Add(gsi); } #endregion } }