dyngoose
Version:
Elegant DynamoDB object modeling for Typescript
194 lines (166 loc) • 6.64 kB
text/typescript
import { type CreateTableCommandInput, type GlobalSecondaryIndex, type KeySchemaElement, type LocalSecondaryIndex } from '@aws-sdk/client-dynamodb'
import { uniqBy } from 'lodash'
import { type Attribute } from '../attribute'
import { SchemaError } from '../errors'
import { type Schema } from './schema'
import { DEFAULT_READ_CAPACITY, DEFAULT_WRITE_CAPACITY } from './defaults'
export function createTableInput(schema: Schema, forCloudFormation = false): CreateTableCommandInput {
const params: CreateTableCommandInput = {
TableName: schema.name,
AttributeDefinitions: [
{
AttributeName: schema.primaryKey.hash.name,
AttributeType: schema.primaryKey.hash.type.type,
},
],
KeySchema: [
{
AttributeName: schema.primaryKey.hash.name,
KeyType: 'HASH',
},
],
}
if (schema.options.billingMode === 'PAY_PER_REQUEST') {
params.BillingMode = 'PAY_PER_REQUEST'
} else if (schema.options.billingMode === 'PROVISIONED') {
params.ProvisionedThroughput = {
ReadCapacityUnits: schema.throughput?.read ?? DEFAULT_READ_CAPACITY,
WriteCapacityUnits: schema.throughput?.write ?? DEFAULT_WRITE_CAPACITY,
}
}
if (schema.primaryKey.range != null) {
params.AttributeDefinitions!.push({
AttributeName: schema.primaryKey.range.name,
AttributeType: schema.primaryKey.range.type.type,
})
params.KeySchema!.push({
AttributeName: schema.primaryKey.range.name,
KeyType: 'RANGE',
})
}
if (schema.options.encrypted === true) {
if (forCloudFormation) {
(params as any).SSESpecification = {
SSEEnabled: true,
}
} else {
params.SSESpecification = { Enabled: true }
}
}
if (schema.options.backup === true) {
if (forCloudFormation) {
(params as any).PointInTimeRecoverySpecification = {
PointInTimeRecoveryEnabled: true,
}
}
}
if (schema.options.stream != null) {
if (typeof schema.options.stream === 'boolean') {
params.StreamSpecification = {
StreamEnabled: true,
StreamViewType: 'NEW_AND_OLD_IMAGES',
}
} else {
params.StreamSpecification = schema.options.stream
}
// CloudFormation template syntax is slightly different than the input for the CreateTable operation
// so we delete the StreamEnabled because it is not valid within CloudFormation templates
if (forCloudFormation) {
delete (params.StreamSpecification as any).StreamEnabled
}
}
if (schema.localSecondaryIndexes.length > 0) {
params.LocalSecondaryIndexes = schema.localSecondaryIndexes.map((indexMetadata) => {
const KeySchema: KeySchemaElement[] = [
{
AttributeName: schema.primaryKey.hash.name,
KeyType: 'HASH',
},
{
AttributeName: indexMetadata.range.name,
KeyType: 'RANGE',
},
]
// make sure this attribute is defined in the AttributeDefinitions
if (params.AttributeDefinitions!.find((ad) => indexMetadata.range.name === ad.AttributeName) == null) {
params.AttributeDefinitions!.push({
AttributeName: indexMetadata.range.name,
AttributeType: indexMetadata.range.type.type,
})
}
const index: LocalSecondaryIndex = {
IndexName: indexMetadata.name,
KeySchema,
Projection: {
ProjectionType: indexMetadata.projection == null ? 'ALL' : indexMetadata.projection,
},
// Projection: indexMetadata.projection,
}
if (indexMetadata.nonKeyAttributes != null && indexMetadata.nonKeyAttributes.length > 0) {
if (indexMetadata.projection !== 'INCLUDE') {
throw new SchemaError(`Invalid configuration for LocalSecondaryIndex ${schema.name}/${indexMetadata.name}. nonKeyAttributes can only be used with projection INCLUDE.`)
}
index.Projection!.NonKeyAttributes = indexMetadata.nonKeyAttributes
}
return index
})
}
if (schema.globalSecondaryIndexes.length > 0) {
params.GlobalSecondaryIndexes = schema.globalSecondaryIndexes.map((indexMetadata) => {
const KeySchema: KeySchemaElement[] = [{
AttributeName: indexMetadata.hash.name,
KeyType: 'HASH',
}]
// make sure this attribute is defined in the AttributeDefinitions
if (params.AttributeDefinitions!.find((ad) => indexMetadata.hash.name === ad.AttributeName) == null) {
params.AttributeDefinitions!.push({
AttributeName: indexMetadata.hash.name,
AttributeType: indexMetadata.hash.type.type,
})
}
if (indexMetadata.range != null) {
// make sure the rangeKey is defined in the AttributeDefinitions
if (params.AttributeDefinitions!.find((ad) => (indexMetadata.range as Attribute<any>).name === ad.AttributeName) == null) {
params.AttributeDefinitions!.push({
AttributeName: indexMetadata.range.name,
AttributeType: indexMetadata.range.type.type,
})
}
KeySchema.push({
AttributeName: indexMetadata.range.name,
KeyType: 'RANGE',
})
}
// by default, indexes will share the same throughput as the table
const throughput = indexMetadata.throughput == null ? schema.throughput : indexMetadata.throughput
const index: GlobalSecondaryIndex = {
IndexName: indexMetadata.name,
KeySchema,
Projection: {
ProjectionType: indexMetadata.projection == null ? 'ALL' : indexMetadata.projection,
},
}
if (schema.options.billingMode !== 'PAY_PER_REQUEST') {
index.ProvisionedThroughput = {
ReadCapacityUnits: throughput?.read ?? DEFAULT_READ_CAPACITY,
WriteCapacityUnits: throughput?.write ?? DEFAULT_WRITE_CAPACITY,
}
}
if (indexMetadata.nonKeyAttributes != null && indexMetadata.nonKeyAttributes.length > 0) {
if (indexMetadata.projection !== 'INCLUDE') {
throw new SchemaError(`Invalid configuration for GlobalSecondaryIndex ${schema.name}/${indexMetadata.name}. nonKeyAttributes can only be used with projection INCLUDE.`)
}
index.Projection!.NonKeyAttributes = indexMetadata.nonKeyAttributes
}
return index
})
}
if (forCloudFormation && schema.timeToLiveAttribute != null) {
(params as any).TimeToLiveSpecification = {
AttributeName: schema.timeToLiveAttribute.name,
Enabled: true,
}
}
params.AttributeDefinitions = uniqBy(params.AttributeDefinitions, (attr) => attr.AttributeName)
return params
}