-
Notifications
You must be signed in to change notification settings - Fork 55
add support for Mapper initialization #1237
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c7f1d00
32006a1
b9b6ed1
569face
a8fff00
6b617df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,18 +4,105 @@ | |
| */ | ||
| package aws.sdk.kotlin.hll.dynamodbmapper | ||
|
|
||
| import aws.sdk.kotlin.hll.dynamodbmapper.schemas.ItemSchema | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.internal.DynamoDbMapperImpl | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.internal.MapperConfigBuilderImpl | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.Interceptor | ||
| import aws.sdk.kotlin.services.dynamodb.DynamoDbClient | ||
|
|
||
| // TODO refactor to interface, add support for multi-table operations, document, add unit tests | ||
| public class DynamoDbMapper(public val client: DynamoDbClient) { | ||
| public fun <I, PK> getTable( | ||
| /** | ||
| * A high-level client for DynamoDB which maps custom data types into DynamoDB attributes and vice versa. | ||
| */ | ||
| public interface DynamoDbMapper { | ||
| public companion object { | ||
| /** | ||
| * Instantiate a new [DynamoDbMapper] | ||
| * @param client The low-level DynamoDB client to use for underlying calls to the service | ||
| * @param config A DSL configuration block | ||
| */ | ||
| public operator fun invoke(client: DynamoDbClient, config: Config.Builder.() -> Unit = { }): DynamoDbMapper = | ||
| DynamoDbMapperImpl(client, Config(config)) | ||
| } | ||
|
|
||
| /** | ||
| * The low-level DynamoDB client used for underlying calls to the service | ||
| */ | ||
| public val client: DynamoDbClient | ||
|
|
||
| /** | ||
| * Get a [Table] reference for performing table operations | ||
| * @param T The type of objects which will be read from and/or written to this table | ||
| * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] | ||
| * @param name The name of the table | ||
| * @param schema The [ItemSchema] which describes the table, its keys, and how items are converted | ||
| */ | ||
| public fun <T, PK> getTable( | ||
| name: String, | ||
| schema: ItemSchema.PartitionKey<I, PK>, | ||
| ): Table.PartitionKey<I, PK> = Table(client, name, schema) | ||
| schema: ItemSchema.PartitionKey<T, PK>, | ||
| ): Table.PartitionKey<T, PK> | ||
|
|
||
| public fun <I, PK, SK> getTable( | ||
| /** | ||
| * Get a [Table] reference for performing table operations | ||
| * @param T The type of objects which will be read from and/or written to this table | ||
| * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] | ||
| * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] | ||
| * @param name The name of the table | ||
| * @param schema The [ItemSchema] which describes the table, its keys, and how items are converted | ||
| */ | ||
| public fun <T, PK, SK> getTable( | ||
| name: String, | ||
| schema: ItemSchema.CompositeKey<I, PK, SK>, | ||
| ): Table.CompositeKey<I, PK, SK> = Table(client, name, schema) | ||
| schema: ItemSchema.CompositeKey<T, PK, SK>, | ||
| ): Table.CompositeKey<T, PK, SK> | ||
|
|
||
| // TODO add multi-table operations like batchGetItem, transactWriteItems, etc. | ||
|
|
||
| /** | ||
| * The immutable configuration for a [DynamoDbMapper] instance | ||
| */ | ||
| public interface Config { | ||
| public companion object { | ||
| /** | ||
| * Instantiate a new [Config] object | ||
| * @param config A DSL block for setting properties of the config | ||
| */ | ||
| public operator fun invoke(config: Builder.() -> Unit = { }): Config = | ||
| Builder().apply(config).build() | ||
| } | ||
|
|
||
| /** | ||
| * A list of [Interceptor] instances which will be applied to operations as they move through the request | ||
| * pipeline. | ||
| */ | ||
| public val interceptors: List<Interceptor<*, *, *, *, *>> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Will the types here become more specific than
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll take a second look at this when I'm implementing the request pipeline but my gut feeling is that this is the expected type. Each of these type params will vary based on the high-level object type ( |
||
|
|
||
| /** | ||
| * Convert this immutable configuration into a mutable [Builder] object. Updates made to the mutable builder | ||
| * properties will not affect this instance. | ||
| */ | ||
| public fun toBuilder(): Builder | ||
|
|
||
| /** | ||
| * A mutable configuration builder for a [DynamoDbMapper] instance | ||
| */ | ||
| public interface Builder { | ||
| public companion object { | ||
| /** | ||
| * Instantiate a new [Builder] object | ||
| */ | ||
| public operator fun invoke(): Builder = MapperConfigBuilderImpl() | ||
| } | ||
|
|
||
| /** | ||
| * A list of [Interceptor] instances which will be applied to operations as they move through the request | ||
| * pipeline. | ||
| */ | ||
| public var interceptors: MutableList<Interceptor<*, *, *, *, *>> | ||
|
|
||
| /** | ||
| * Builds this mutable [Builder] object into an immutable [Config] object. Changes made to this instance do | ||
| * not affect the built instance. | ||
| */ | ||
| public fun build(): Config | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,90 +4,61 @@ | |
| */ | ||
| package aws.sdk.kotlin.hll.dynamodbmapper | ||
|
|
||
| import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.model.Item | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.model.itemOf | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.schemas.ItemSchema | ||
| import aws.sdk.kotlin.services.dynamodb.DynamoDbClient | ||
| import aws.sdk.kotlin.services.dynamodb.getItem | ||
| import aws.sdk.kotlin.services.dynamodb.paginators.items | ||
| import aws.sdk.kotlin.services.dynamodb.paginators.scanPaginated | ||
| import aws.sdk.kotlin.services.dynamodb.putItem | ||
| import kotlinx.coroutines.flow.Flow | ||
| import kotlinx.coroutines.flow.map | ||
|
|
||
| // TODO refactor to interface, add support for all operations, document, add unit tests | ||
| public sealed class Table<I>(public val client: DynamoDbClient, public val name: String) { | ||
| public abstract val schema: ItemSchema<I> | ||
| /** | ||
| * Represents a table in DynamoDB and an associated item schema. Operations on this table will invoke low-level | ||
| * operations after mapping objects to items and vice versa. | ||
| * @param T The type of objects which will be read from and/or written to this table | ||
| */ | ||
| public interface Table<T> { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Should this be a sealed interface?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could be sealed but that might complicate users' unit testing. My goal is to maintain maximum flexibility in the public API by leaving as many types as possible/sensible open interfaces which can be mocked without a library. Right now I don't have a use case which requires knowing all the concrete implementations of |
||
| /** | ||
| * The [DynamoDbMapper] which holds the underlying DynamoDB service client used to invoke operations | ||
| */ | ||
| public val mapper: DynamoDbMapper | ||
|
|
||
| public companion object { | ||
| public operator fun <I, PK> invoke( | ||
| client: DynamoDbClient, | ||
| name: String, | ||
| schema: ItemSchema.PartitionKey<I, PK>, | ||
| ): PartitionKey<I, PK> = PartitionKey(client, name, schema) | ||
| /** | ||
| * The name of this table | ||
| */ | ||
| public val name: String | ||
|
|
||
| public operator fun <I, PK, SK> invoke( | ||
| client: DynamoDbClient, | ||
| name: String, | ||
| schema: ItemSchema.CompositeKey<I, PK, SK>, | ||
| ): CompositeKey<I, PK, SK> = CompositeKey(client, name, schema) | ||
| } | ||
| /** | ||
| * The [ItemSchema] for this table which describes how to map objects to items and vice versa | ||
| */ | ||
| public val schema: ItemSchema<T> | ||
|
|
||
| public fun scan(): Flow<I> { | ||
| val resp = client.scanPaginated { | ||
| tableName = name | ||
| } | ||
| return resp.items().map { schema.converter.fromItem(Item(it)) } | ||
| } | ||
| // TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc. | ||
|
|
||
| internal suspend fun getItem(key: Item): I? { | ||
| val resp = client.getItem { | ||
| tableName = name | ||
| this.key = key | ||
| } | ||
| return resp.item?.let { schema.converter.fromItem(Item(it)) } | ||
| } | ||
| public suspend fun getItem(key: Item): T? | ||
|
|
||
| @Suppress("INAPPLICABLE_JVM_NAME") | ||
| @JvmName("getItemByKeyItem") | ||
| public abstract suspend fun getItem(keyItem: I): I? | ||
| @JvmName("getItemByKeyObj") | ||
| public suspend fun getItem(keyObj: T): T? | ||
|
|
||
| public suspend fun putItem(item: I) { | ||
| client.putItem { | ||
| tableName = name | ||
| this.item = schema.converter.toItem(item) | ||
| } | ||
| } | ||
|
|
||
| public class PartitionKey<I, PK> internal constructor( | ||
| client: DynamoDbClient, | ||
| name: String, | ||
| override val schema: ItemSchema.PartitionKey<I, PK>, | ||
| ) : Table<I>(client, name) { | ||
| private val keyAttributeNames = setOf(schema.partitionKey.name) | ||
| public suspend fun putItem(obj: T) | ||
|
|
||
| @Suppress("INAPPLICABLE_JVM_NAME") | ||
| @JvmName("getItemByKeyItem") | ||
| override suspend fun getItem(keyItem: I): I? = | ||
| getItem(schema.converter.toItem(keyItem, keyAttributeNames)) | ||
| public fun scan(): Flow<T> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: How will this take into account filtering, exclusive start key, etc?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry for not clarifying more in the PR description but all the operation methods below the The eventual answer to your question will be that each high-level operation will have a canonical method that accepts a high-level request object and returns a high-level response object. Then there will be convenience methods for a handful of APIs which deal in more primitive types. The eventual |
||
|
|
||
| public suspend fun getItem(partitionKey: PK): I? = | ||
| getItem(itemOf(schema.partitionKey.toField(partitionKey))) | ||
| /** | ||
| * Represents a table whose primary key is a single partition key | ||
| * @param T The type of objects which will be read from and/or written to this table | ||
| * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] | ||
| */ | ||
| public interface PartitionKey<T, PK> : Table<T> { | ||
| // TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc. | ||
| public suspend fun getItem(partitionKey: PK): T? | ||
| } | ||
|
|
||
| public class CompositeKey<I, PK, SK> internal constructor( | ||
| client: DynamoDbClient, | ||
| name: String, | ||
| override val schema: ItemSchema.CompositeKey<I, PK, SK>, | ||
| ) : Table<I>(client, name) { | ||
| private val keyAttributeNames = setOf(schema.partitionKey.name, schema.sortKey.name) | ||
|
|
||
| @Suppress("INAPPLICABLE_JVM_NAME") | ||
| @JvmName("getItemByKeyItem") | ||
| override suspend fun getItem(keyItem: I): I? = | ||
| getItem(schema.converter.toItem(keyItem, keyAttributeNames)) | ||
|
|
||
| public suspend fun getItem(partitionKey: PK, sortKey: SK): I? = | ||
| getItem(itemOf(schema.partitionKey.toField(partitionKey), schema.sortKey.toField(sortKey))) | ||
| /** | ||
| * Represents a table whose primary key is a composite of a partition key and a sort key | ||
| * @param T The type of objects which will be read from and/or written to this table | ||
| * @param PK The type of the partition key property, either [String], [Number], or [ByteArray] | ||
| * @param SK The type of the sort key property, either [String], [Number], or [ByteArray] | ||
| */ | ||
| public interface CompositeKey<T, PK, SK> : Table<T> { | ||
| // TODO reimplement operations to use pipeline, extension functions where appropriate, docs, etc. | ||
| public suspend fun getItem(partitionKey: PK, sortKey: SK): T? | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| /* | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package aws.sdk.kotlin.hll.dynamodbmapper.internal | ||
|
|
||
| import aws.sdk.kotlin.hll.dynamodbmapper.DynamoDbMapper | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.items.ItemSchema | ||
| import aws.sdk.kotlin.hll.dynamodbmapper.pipeline.Interceptor | ||
| import aws.sdk.kotlin.services.dynamodb.DynamoDbClient | ||
|
|
||
| internal data class DynamoDbMapperImpl( | ||
| override val client: DynamoDbClient, | ||
| private val config: DynamoDbMapper.Config, | ||
| ) : DynamoDbMapper { | ||
| override fun <T, PK> getTable(name: String, schema: ItemSchema.PartitionKey<T, PK>) = | ||
| TableImpl.PartitionKeyImpl(this, name, schema) | ||
|
|
||
| override fun <T, PK, SK> getTable(name: String, schema: ItemSchema.CompositeKey<T, PK, SK>) = | ||
| TableImpl.CompositeKeyImpl(this, name, schema) | ||
| } | ||
|
|
||
| internal data class MapperConfigImpl( | ||
| override val interceptors: List<Interceptor<*, *, *, *, *>>, | ||
| ) : DynamoDbMapper.Config { | ||
| override fun toBuilder() = DynamoDbMapper | ||
| .Config | ||
| .Builder() | ||
| .also { it.interceptors = interceptors.toMutableList() } | ||
| } | ||
|
|
||
| internal class MapperConfigBuilderImpl : DynamoDbMapper.Config.Builder { | ||
| override var interceptors = mutableListOf<Interceptor<*, *, *, *, *>>() | ||
|
|
||
| override fun build() = MapperConfigImpl(interceptors.toList()) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: Any reason to default this to
{}instead of nullable?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Simply wanting to avoid nullability when a unit function accomplishes the same thing.