dynamo

package module
v1.7.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 9, 2022 License: MIT Imports: 11 Imported by: 0

README

dynamo

The library implements a simple key-value abstraction to store algebraic, linked-data data types at AWS storage services: AWS DynamoDB and AWS S3.

Version Documentation Build Status Git Hub Coverage Status Go Report Card Maintainability

Inspiration

The library encourages developers to use Golang struct to define domain models, write correct, maintainable code. Using this library, the application can achieve the ideal data model that would require a single request to DynamoDB and model one-to-one, one-to-many and even many-to-many relations. The library uses generic programming style to implement actual storage I/O, while expose external domain object as [T dynamo.Thing] with implicit conversion back and forth between a concrete struct(s). The library uses AWS Golang SDK v2 under the hood.

Essentially, the library implement a following generic key-value trait to access domain objects.

type KeyVal[T dynamo.Thing] interface {
  Put(T) error
  Get(T) (*T, error)
  Remove(T) error
  Update(T) (*T, error)
  Match(T) Seq[T]
}

The library philosophy and use-cases are covered in depth at the post How To Model Any Relational Data in DynamoDB With dynamo library or continue reading the Getting started section.

Getting started

The library requires Go 1.18 or later due to usage of generics.

The latest version of the library is available at its main branch. All development, including new features and bug fixes, take place on the main branch using forking and pull requests as described in contribution guidelines. The stable version is available via Golang modules.

Data types definition

Data types definition is an essential part of development with dynamo library. Golang structs declares domain of your application. Public fields are serialized into DynamoDB attributes, the field tag dynamodbav controls marshal/unmarshal processes.

The library demands from each structure implementation of Thing interface. This type acts as struct annotation -- Golang compiler raises an error at compile time if other data type is supplied to the dynamo library. Secondly, each structure defines unique "composite primary key". The library encourages definition of both partition and sort keys using a special data type curie.IRI. This type is a synonym to compact Internationalized Resource Identifiers, which facilitates linked-data, hierarchical structures and cheap relations between data items. curie.IRI is a synonym to the built-in string type so that anything castable to string suite to model the keys as alternative solution.

import "github.com/fogfish/dynamo"

type Person struct {
  Org     curie.IRI `dynamodbav:"prefix,omitempty"`
  ID      curie.IRI `dynamodbav:"suffix,omitempty"`
  Name    string    `dynamodbav:"name,omitempty"`
  Age     int       `dynamodbav:"age,omitempty"`
  Address string    `dynamodbav:"address,omitempty"`
}

//
// Identity implements thing interface
func (p Person) HashKey() curie.IRI { return p.Org }
func (p Person) SortKey() curie.IRI { return p.ID }

//
// this data type is a normal Golang struct
// just create an instance, fill required fields
var person := Person{
  Org:     curie.IRI("University:Kiel"),
  ID:      curie.IRI("Professor:8980789222"),
  Name:    "Verner Pleishner",
  Age:     64,
  Address: "Blumenstrasse 14, Berne, 3013",
}

This is it! Your application is ready to read/write data to/form DynamoDB tables.

DynamoDB I/O

Please see and try examples. Its cover all basic use-cases with runnable code snippets, check the post How To Model Any Relational Data in DynamoDB With dynamo library for deep-dive into library philosophy.

go run examples/keyval/main.go ddb:///my-table

The following code snippet shows a typical I/O patterns

import (
  "github.com/fogfish/dynamo"
  "github.com/fogfish/dynamo/keyval"
)

//
// Create dynamodb client and bind it with the table.
// The client is type-safe and support I/O with a single type (e.g. Person).
// Use URI notation to specify the diver (ddb://) and the table (/my-table).
db := keyval.Must(
  keyval.New[Person](
    dynamo.WithURI("ddb:///my-table"),
  ),
)

//
// Write the struct with Put
if err := db.Put(person); err != nil {
}

//
// Lookup the struct using Get. This function takes input structure as key
// and return a new copy upon the completion. The only requirement - ID has to
// be defined.
val, err := db.Get(
  Person{
    Org: curie.IRI("University:Kiel"),
    ID:  curie.IRI("Professor:8980789222"),
  },
)

switch {
case nil:
  // success
case recoverNotFound(err):
  // not found
default:
  // other i/o error
}

//
// Apply a partial update using Update function. This function takes 
// a partially defined structure, patches the instance at storage and 
// returns remaining attributes.
val, err := db.Update(
  Person{
    Org:     curie.IRI("University:Kiel"),
    ID:      curie.IRI("Professor:8980789222"),
    Address: "Viktoriastrasse 37, Berne, 3013",
  }
)

if err != nil { /* ... */ }

//
// Remove the struct using Remove give partially defined struct with ID
err := db.Remove(
  Person{
    Org: curie.IRI("University:Kiel"),
    ID:  curie.IRI("Professor:8980789222"),
  }
)

if err != nil { /* ... */ }
Error Handling

The library enforces for "assert errors for behavior, not type" as the error handling strategy, see the post for details.

Use following behaviors to recover from errors:

type ErrorCode interface{ ErrorCode() string }

type NotFound interface { NotFound() string }

type PreConditionFailed interface { PreConditionFailed() bool }

type Conflict interface { Conflict() bool }

type Gone interface { Gone() bool }
Hierarchical structures

The library support definition of A ⟼ B relation for data elements. Let's consider message threads as a classical examples for such hierarchies:

A
├ B
├ C
│ ├ D  
│ └ E
│   └ F
└ G

Composite sort key is core concept to organize hierarchies. It facilitates linked-data, hierarchical structures and cheap relations between data items. An application declares node path using composite sort key design pattern. For example, the root is thread:A, 2nd rank node ⟨thread:A, B⟩, 3rd rank node ⟨thread:A, C/D⟩ and so on ⟨thread:A, C/E/F⟩. Each id declares partition and sub nodes. The library implement a Match function, supply the node identity and it returns sequence of child elements.

//
// Match uses partition key to match DynamoDB entries. It returns a sequence of 
// data type instances. FMap is an utility it takes a closure function. 
db.Match(Message{Thread: "thread:A"}).FMap(/* ... */)

//
// Match uses partition key and partial sort key to match DynamoDB entries. 
db.Match(Message{Thread: "thread:A", ID: "C"}).FMap(/* ... */)


//
// Type aliases is the best approach to lift generic sequence in type safe one.
type Messages []Message

// Join is a monoid to append generic element into sequence 
func (seq *Messages) Join(val *Message) error {
  *seq = append(*seq, *val)
  return nil
}

// and final magic to discover hierarchy of elements
seq := Messages{}
db.Match(Message{Thread: "thread:A", ID: "C/E"}).FMap(seq.Join)

See the go doc for api spec and advanced example app.

Sequences and Pagination

Hierarchical structures is the way to organize collections, lists, sets, etc. The Match returns a lazy Sequence that represents your entire collection. Sometimes, your need to split the collection into sequence of pages.

// 1. Set the limit on the stream 
seq := db.Match(Message{Thread: "thread:A", ID: "C"}).Limit(25)
// 2. Consume the stream
seq.FMap(persons.Join)
// 3. Read cursor value
cursor := seq.Cursor()


// 4. Continue I/O with a new stream, supply the cursor
seq := db.Match(Message{Thread: "thread:A", ID: "C"}).Limit(25).Continue(cursor)
Linked data

Cross-linking of structured data is an essential part of type safe domain driven design. The library helps developers to model relations between data instances using familiar data type.

type Person struct {
  Org     curie.IRI  `dynamodbav:"prefix,omitempty"`
  ID      curie.IRI  `dynamodbav:"suffix,omitempty"`
  Leader  *curie.IRI `dynamodbav:"leader,omitempty"`
}

ID and Leader are sibling, equivalent data types. ID is only used as primary identity, Leader is a "pointer" to linked-data. The library advices usage of compact Internationalized Resource Identifiers (curie.IRI) for this purpose. Semantic Web publishes structured data using this type so that it can be interlinked by applications.

Type projections

Often, there is an established system of the types in the application. It is not convenient to inject dependencies to the dynamo library. Also, the usage of secondary indexes requires multiple projections of core type.

// 
// original core type
type Person struct {
  Org     string `dynamodbav:"prefix,omitempty"`
  ID      string `dynamodbav:"suffix,omitempty"`
  Name    string `dynamodbav:"name,omitempty"`
  Age     int    `dynamodbav:"age,omitempty"`
  Country string `dynamodbav:"country,omitempty"`
}

//
// the core type projection that uses ⟨Org, ID⟩ as composite key
// e.g. this projection supports writes to DynamoDB table
type dbPerson Person

func (p dbPerson) HashKey() curie.IRI { return curie.IRI(p.Org) }
func (p dbPerson) SortKey() curie.IRI { return curie.IRI(p.ID) }

//
// the core type projection that uses ⟨Org, Name⟩ as composite key
// e.g. the projection support lookup of employer
type dbNamedPerson Person

func (p dbNamedPerson) HashKey() curie.IRI { return curie.IRI(p.Org) }
func (p dbNamedPerson) SortKey() curie.IRI { return curie.IRI(p.Name) }

//
// the core type projection that uses ⟨Country, Name⟩ as composite key
type dbCitizen Person

func (p dbCitizen) HashKey() curie.IRI { return curie.IRI(p.Country) }
func (p dbCitizen) SortKey() curie.IRI { return curie.IRI(p.Name) }
Custom codecs for core domain types

Development of complex Golang application might lead developers towards Standard Package Layout. It becomes extremely difficult to isolate dependencies from core data types to this library and AWS SDK. The library support serialization of core type to dynamo using custom codecs

/*** core.go ***/

// 1. complex domain type is defined
type ID struct {/* ... */}

// 2. structure with core types is defined, no deps to dynamo library
type Person struct {
  Org      ID  `dynamodbav:"prefix,omitempty"`
  ID       ID  `dynamodbav:"suffix,omitempty"`
}

/*** aws/ddb/ddb.go ***/

// 3. declare codecs for complex core domain type 
type id core.ID

func (id) MarshalDynamoDBAttributeValue() (types.AttributeValue, error) {/* ...*/}
func (*id) UnmarshalDynamoDBAttributeValue(types.AttributeValue) error {/* ...*/}

// aws/ddb/ddb.go
// 2. type alias to core type implements dynamo custom codec
type dbPerson Person

// 3. custom codec for structure field is defined 
var codecHashKey, codecSortKey = dynamo.Codec2[dbPerson, id, id]("Org", "ID")

// 4. use custom codec
func (p dbPerson) MarshalDynamoDBAttributeValue() (types.AttributeValue, error) {
  type tStruct dbPerson
  return dynamo.Encode(av, tStruct(p),
    codecHashKey.Encode(id(p.Org)),
    codecSortKey.Encode(id(p.ID))),
  )
}

func (x *dbPerson) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error {
  type tStruct *dbPerson
  return dynamo.Decode(av, tStruct(x),
    codecHashKey.Decode((*id)(&x.Org)),
    codecSortKey.Decode((*id)(&x.ID))),
  )
}
Optimistic Locking

Optimistic Locking is a lightweight approach to ensure causal ordering of read, write operations to database. AWS made a great post about Optimistic Locking with Version Number.

The dynamo library implements type safe conditional expressions to achieve optimistic locking. This feature is vital when your serverless application concurrently updates same entity in the database.

Let's consider a following example.

type Person struct {
  Org     string `dynamodbav:"prefix,omitempty"`
  ID      string `dynamodbav:"suffix,omitempty"`
  Name    string `dynamodbav:"anothername,omitempty"`
}

An optimistic locking on this structure is straightforward from DynamoDB perspective. Just make a request with conditional expression:

&dynamodb.UpdateItemInput{
  ConditionExpression: "anothername = :anothername",
  ExpressionAttributeValues: /* ":anothername" : {S: "Verner Pleishner"} */
}

However, the application operates with struct types. How to define a condition expression on the field Name? Golang struct defines and refers the field by Name but DynamoDB stores it under the attribute anothername. Struct field dynamodbav tag specifies serialization rules. Golang does not support a typesafe approach to build a correspondence between Nameanothername. Developers have to utilize dynamodb attribute name(s) in conditional expression and Golang struct name in rest of the code. It becomes confusing and hard to maintain. The library defines set of helper types and functions to declare and use conditional expression in type safe manner:

type Person struct {
  Org     string `dynamodbav:"prefix,omitempty"`
  ID      string `dynamodbav:"suffix,omitempty"`
  Name    string `dynamodbav:"anothername,omitempty"`
}
var Name = dynamo.Schema1[Person, string]("Name")

val, err := db.Update(&person, Name.Eq("Verner Pleishner"))
switch err.(type) {
case nil:
  // success
case dynamo.PreConditionFailed:
  // not found
default:
  // other i/o error
}

See the go doc for all supported constraints.

Configure DynamoDB

The dynamo library is optimized to operate with generic Dynamo DB that declares both partition and sort keys with fixed names. Use the following schema:

const Schema = (): ddb.TableProps => ({
  tableName: 'my-table',
  partitionKey: {type: ddb.AttributeType.STRING, name: 'prefix'},
  sortKey: {type: ddb.AttributeType.STRING, name: 'suffix'},
})

If table uses other names for partitionKey and sortKey then connect URI allows to re-declare them

//
// Create client and bind it with DynamoDB the table
db := keyval.Must(
  keyval.New[Person](
    dynamo.WithURI("ddb:///my-table?prefix=someHashKey&suffix=someSortKe"),
  ),
)

The following post discusses in depth and shows example DynamoDB table configuration and covers aspect of secondary indexes.

AWS S3 Support

The library advances its simple I/O interface to AWS S3 bucket, allowing to persist data types to multiple storage simultaneously.

//
// Create client and bind it with DynamoDB the table
db := keyval.Must(keyval.New("ddb:///my-table"))

//
// Create client and bind it with S3 bucket
s3 := keyval.Must(keyval.New("s3:///my-bucket"))

There are few fundamental differences about AWS S3 bucket

  • use s3 schema of connection URI;
  • compose primary key is serialized to S3 bucket path. (e.g. ⟨thread:A, C/E/F⟩ ⟼ thread/A/_/C/E/F);
  • storage persists struct to JSON, use json field tags to specify serialization rules;
  • optimistic locking is not supported yet, any conditional expression is silently ignored;
  • Update is not thread safe.

How To Contribute

The library is MIT licensed and accepts contributions via GitHub pull requests:

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Added some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

The build and testing process requires Go version 1.13 or later.

build and test library.

git clone https://github.com/fogfish/dynamo
cd dynamo
go test
commit message

The commit message helps us to write a good release note, speed-up review process. The message should address two question what changed and why. The project follows the template defined by chapter Contributing to a Project of Git book.

bugs

If you experience any issues with the library, please let us know via GitHub issues. We appreciate detailed and accurate reports that help us to identity and replicate the issue.

License

See LICENSE

Documentation

Overview

Package dynamo implements a simple key-value abstraction to store algebraic data types with AWS services:

↣ AWS DynamoDB

↣ AWS S3

Inspiration

The library encourages developers to use Golang struct to define domain models, write correct, maintainable code. Using the library, the application can achieve the ideal data model that would require a single request to DynamoDB and model one-to-one, one-to-many and even many-to-many relations. The library uses generic programming style to implement actual storage I/O, while expose external domain object as `[T dynamo.Thing]` with implicit conversion back and forth between a concrete struct(s).

Essentially, it implement a following generic key-value trait to access domain objects. The library AWS Go SDK under the hood

type KeyVal[T any] interface {
  Put(T) error
  Get(T) (*T, error)
  Remove(T) error
  Update(T): (*T, error)
  Match(T): Seq[T]
}

Getting started

Define an application domain model using product types, which are strongly expressed by struct in Go.

type Person struct {
  Org     curie.IRI `dynamodbav:"prefix,omitempty"`
  ID      curie.IRI `dynamodbav:"suffix,omitempty"`
  Name    string    `dynamodbav:"name,omitempty"`
  Age     int       `dynamodbav:"age,omitempty"`
  Address string    `dynamodbav:"address,omitempty"`
}

Make sure that defined type implements dynamo.Thing interface for identity

func (p Person) HashKey() curie.IRI { return p.Org }
func (p Person) SortKey() curie.IRI { return p.ID }

Use DynamoDB attributes from AWS Go SDK to specify marshalling rules https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/dynamodbattribute.

Create an implicit I/O endpoint to Dynamo DB table

db := keyval.New[Person](dynamo.WithURI("ddb:///my-table"))

Creates a new entity, or replaces an old entity with a new value.

err := db.Put(
  Person{
    Org:      curie.IRI("test"),
    ID:       curie.IRI("8980789222"),
    Name:     "Verner Pleishner",
    Age:      64,
    Address:  "Blumenstrasse 14, Berne, 3013",
  }
)

Lookup the struct using Get. This function takes input structure as key and return a new copy upon the completion. The only requirement - ID has to be defined.

val, err := db.Get(Person{Org: curie.IRI("test"), ID: curie.IRI("8980789222")})
switch err.(type) {
case nil:
  // success
case dynamo.NotFound:
  // not found
default:
  // other i/o error
}

Remove the entity

err := db.Remove(Person{Org: curie.IRI("test"), ID: curie.IRI("8980789222")})

Apply a partial update using Update function. This function takes a partially defined structure, patches the instance at storage and returns remaining attributes.

person := Person{
  Org:     "test",
  ID:      "8980789222"
  Address: "Viktoriastrasse 37, Berne, 3013",
}
val, err := db.Update(person)
if err != nil { ... }

Use following DynamoDB schema:

  const Schema = (): ddb.TableProps => ({
	   partitionKey: {type: ddb.AttributeType.STRING, name: 'prefix'},
	   sortKey: {type: ddb.AttributeType.STRING, name: 'suffix'},
	   tableName: 'my-table',
	   readCapacity: 1,
	   writeCapacity: 1,
  })

See README at https://github.com/fogfish/dynamo

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Codec10 added in v1.5.0

func Codec10[T Thing, A, B, C, D, E, F, G, H, I, J any](a, b, c, d, e, f, g, h, i, j string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
	CodecOf[T, D],
	CodecOf[T, E],
	CodecOf[T, F],
	CodecOf[T, G],
	CodecOf[T, H],
	CodecOf[T, I],
	CodecOf[T, J],
)

Codec10 builds Codec for 10 attributes

func Codec2 added in v1.5.0

func Codec2[T Thing, A, B any](a, b string) (
	CodecOf[T, A],
	CodecOf[T, B],
)

Codec2 builds Codec for 2 attributes

func Codec3 added in v1.5.0

func Codec3[T Thing, A, B, C any](a, b, c string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
)

Codec4 builds Codec for 4 attributes

func Codec4 added in v1.5.0

func Codec4[T Thing, A, B, C, D any](a, b, c, d string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
	CodecOf[T, D],
)

Codec4 builds Codec for 4 attributes

func Codec5 added in v1.5.0

func Codec5[T Thing, A, B, C, D, E any](a, b, c, d, e string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
	CodecOf[T, D],
	CodecOf[T, E],
)

Codec5 builds Codec for 5 attributes

func Codec6 added in v1.5.0

func Codec6[T Thing, A, B, C, D, E, F any](a, b, c, d, e, f string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
	CodecOf[T, D],
	CodecOf[T, E],
	CodecOf[T, F],
)

Codec6 builds Codec for 6 attributes

func Codec7 added in v1.5.0

func Codec7[T Thing, A, B, C, D, E, F, G any](a, b, c, d, e, f, g string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
	CodecOf[T, D],
	CodecOf[T, E],
	CodecOf[T, F],
	CodecOf[T, G],
)

Codec7 builds Codec for 7 attributes

func Codec8 added in v1.5.0

func Codec8[T Thing, A, B, C, D, E, F, G, H any](a, b, c, d, e, f, g, h string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
	CodecOf[T, D],
	CodecOf[T, E],
	CodecOf[T, F],
	CodecOf[T, G],
	CodecOf[T, H],
)

Codec8 builds Codec for 8 attributes

func Codec9 added in v1.5.0

func Codec9[T Thing, A, B, C, D, E, F, G, H, I any](a, b, c, d, e, f, g, h, i string) (
	CodecOf[T, A],
	CodecOf[T, B],
	CodecOf[T, C],
	CodecOf[T, D],
	CodecOf[T, E],
	CodecOf[T, F],
	CodecOf[T, G],
	CodecOf[T, H],
	CodecOf[T, I],
)

Codec9 builds Codec for 9 attributes

func Decode added in v1.2.0

func Decode(av types.AttributeValue, val interface{}, coder ...Coder) (err error)

Decode is a helper function to decode core domain types from Dynamo DB format. The helper ensures compact URI de-serialization from DynamoDB schema.

  type MyType struct {
    ID   MyComplexType
    Name MyComplexType
  }
  var ID, Name = dynamo.Codec2[MyType, MyDynamoType, MyDynamoType]("ID", "Name")

  func (x *MyType) UnmarshalDynamoDBAttributeValue(av types.AttributeValue) error {
    type tStruct *MyType
    return dynamo.Decode(av, tStruct(x),
      ID.Decode((*MyDynamoType)(&x.ID)),
			Name.Decode((*MyDynamoType)(&x.Name)),
    )
  }

func Encode added in v1.2.0

func Encode(val interface{}, coder ...Coder) (types.AttributeValue, error)

Encode is a helper function to encode core domain types into struct. The helper ensures compact URI serialization into DynamoDB schema.

  type MyType struct {
    ID   MyComplexType
    Name MyComplexType
  }
  var ID, Name = dynamo.Codec2[MyType, MyDynamoType, MyDynamoType]("ID", "Name")

  func (x MyType) MarshalDynamoDBAttributeValue() (types.AttributeValue, error) {
    type tStruct MyType
    return dynamo.Encode(av, tStruct(x),
      ID.Encode(MyDynamoType(x.ID)),
			Name.Encode(MyDynamoType(x.Name)),
    )
  }

func Schema10 added in v1.5.0

func Schema10[T Thing, A, B, C, D, E, F, G, H, I, J any](a, b, c, d, e, f, g, h, i, j string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
	TypeOf[T, D],
	TypeOf[T, E],
	TypeOf[T, F],
	TypeOf[T, G],
	TypeOf[T, H],
	TypeOf[T, I],
	TypeOf[T, J],
)

Schema10 builds Constrain builder for product type of arity 10

func Schema2 added in v1.5.0

func Schema2[T Thing, A, B any](a, b string) (
	TypeOf[T, A],
	TypeOf[T, B],
)

Schema2 builds Constrain builder for product type of arity 2

func Schema3 added in v1.5.0

func Schema3[T Thing, A, B, C any](a, b, c string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
)

Schema3 builds Constrain builder for product type of arity 3

func Schema4 added in v1.5.0

func Schema4[T Thing, A, B, C, D any](a, b, c, d string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
	TypeOf[T, D],
)

Schema4 builds Constrain builder for product type of arity 4

func Schema5 added in v1.5.0

func Schema5[T Thing, A, B, C, D, E any](a, b, c, d, e string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
	TypeOf[T, D],
	TypeOf[T, E],
)

Schema5 builds Constrain builder for product type of arity 5

func Schema6 added in v1.5.0

func Schema6[T Thing, A, B, C, D, E, F any](a, b, c, d, e, f string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
	TypeOf[T, D],
	TypeOf[T, E],
	TypeOf[T, F],
)

Schema6 builds Constrain builder for product type of arity 6

func Schema7 added in v1.5.0

func Schema7[T Thing, A, B, C, D, E, F, G any](a, b, c, d, e, f, g string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
	TypeOf[T, D],
	TypeOf[T, E],
	TypeOf[T, F],
	TypeOf[T, G],
)

Schema7 builds Constrain builder for product type of arity 7

func Schema8 added in v1.5.0

func Schema8[T Thing, A, B, C, D, E, F, G, H any](a, b, c, d, e, f, g, h string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
	TypeOf[T, D],
	TypeOf[T, E],
	TypeOf[T, F],
	TypeOf[T, G],
	TypeOf[T, H],
)

Schema8 builds Constrain builder for product type of arity 8

func Schema9 added in v1.5.0

func Schema9[T Thing, A, B, C, D, E, F, G, H, I any](a, b, c, d, e, f, g, h, i string) (
	TypeOf[T, A],
	TypeOf[T, B],
	TypeOf[T, C],
	TypeOf[T, D],
	TypeOf[T, E],
	TypeOf[T, F],
	TypeOf[T, G],
	TypeOf[T, H],
	TypeOf[T, I],
)

Schema9 builds Constrain builder for product type of arity 9

Types

type CodecOf added in v1.5.0

type CodecOf[T Thing, A any] interface {
	Decode(*A) Coder
	Encode(A) Coder
}

CodecOf for struct fields, the type implement Encode/Decode primitives. Codec helps to implement semi-automated encoding/decoding algebraic data type into the format compatible with storage.

Let's consider scenario were application uses complex types that skips implementation of marshal/unmarshal protocols. Here the type MyComplexType needs to be casted to MyDynamoType that knows how to marshal/unmarshal the type.

type MyType struct {
  ID   MyComplexType
  Name MyComplexType
}
var ID, Name = dynamo.Codec2[MyType, MyDynamoType, MyDynamoType]("ID", "Name")

func (t MyType) MarshalDynamoDBAttributeValue() (*dynamodb.AttributeValue, error) {
  type tStruct MyType
  return dynamo.Encode(tStruct(p),
    ID.Encode(MyDynamoType(t.ID)),
    Name.Encode(MyDynamoType(t.Name)),
  )
}

func Codec1 added in v1.5.1

func Codec1[T Thing, A any](a string) CodecOf[T, A]

Codec1 builds Codec for 1 attributes

type Coder added in v1.4.0

Coder is a function, applies transformation of generic dynamodb AttributeValue

type Config

type Config struct {
	URI      *URL
	Prefixes curie.Prefixes
	AWS      aws.Config
}

Config options for the connection

func NewConfig added in v1.6.0

func NewConfig(opts ...Option) (*Config, error)

NewConfig creates Config with default options

type Constraint added in v1.6.0

type Constraint[T Thing] interface{ TypeOf(T) }

Constraint is a function that applies conditional expression to storage request. Each storage implements own constrains protocols. The module here defines a few constrain protocol. The structure of the constrain is abstracted away from the client.

See internal/constrain package to see details about its implementation

type KeyVal

type KeyVal[T Thing] interface {
	KeyValReader[T]
	KeyValWriter[T]
}

KeyVal is a generic key-value trait to access domain objects.

type KeyValGetter added in v1.2.0

type KeyValGetter[T Thing] interface {
	Get(context.Context, T) (T, error)
}

KeyValGetter defines read by key notation

type KeyValGetterNoContext added in v1.2.0

type KeyValGetterNoContext[T Thing] interface {
	Get(T) (T, error)
}

KeyValGetterNoContext defines read by key notation

type KeyValNoContext added in v1.2.0

type KeyValNoContext[T Thing] interface {
	KeyValReaderNoContext[T]
	KeyValWriterNoContext[T]
}

KeyValNoContext is a generic key-value trait to access domain objects.

type KeyValPattern

type KeyValPattern[T Thing] interface {
	Match(context.Context, T) Seq[T]
}

KeyValPattern defines simple pattern matching lookup I/O

type KeyValPatternNoContext added in v1.2.0

type KeyValPatternNoContext[T Thing] interface {
	Match(T) Seq[T]
}

KeyValPatternNoContext defines simple pattern matching lookup I/O

type KeyValReader added in v1.2.0

type KeyValReader[T Thing] interface {
	KeyValGetter[T]
	KeyValPattern[T]
}

KeyValReader a generic key-value trait to read domain objects

type KeyValReaderNoContext added in v1.2.0

type KeyValReaderNoContext[T Thing] interface {
	KeyValGetterNoContext[T]
	KeyValPatternNoContext[T]
}

KeyValReaderNoContext a generic key-value trait to read domain objects

type KeyValWriter added in v1.2.0

type KeyValWriter[T Thing] interface {
	Put(context.Context, T, ...Constraint[T]) error
	Remove(context.Context, T, ...Constraint[T]) error
	Update(context.Context, T, ...Constraint[T]) (T, error)
}

KeyValWriter defines a generic key-value writer

type KeyValWriterNoContext added in v1.2.0

type KeyValWriterNoContext[T Thing] interface {
	Put(T, ...Constraint[T]) error
	Remove(T, ...Constraint[T]) error
	Update(T, ...Constraint[T]) (T, error)
}

KeyValWriterNoContext defines a generic key-value writer

type Option added in v1.6.0

type Option func(cfg *Config) error

Option type to configure the connection

func WithAwsConfig added in v1.6.0

func WithAwsConfig(aws aws.Config) Option

WithSession defines AWS I/O Session to be used in the context

func WithPrefixes added in v1.6.0

func WithPrefixes(prefixes curie.Prefixes) Option

WithPrefixes defines prefixes for CURIEs

func WithURI added in v1.6.0

func WithURI(uri string) Option

WithURI defines destination URI

type Seq

type Seq[T Thing] interface {
	SeqLazy[T]
	SeqConfig[T]

	// Sequence transformer
	FMap(func(T) error) error
}

Seq is an interface to transform collection of objects

db.Match(dynamo.NewID("users")).FMap(func(x *T) error { ... })

type SeqConfig

type SeqConfig[T Thing] interface {
	// Limit sequence size to N elements (pagination)
	Limit(int) Seq[T]
	// Continue limited sequence from the cursor
	Continue(Thing) Seq[T]
	// Reverse order of sequence
	Reverse() Seq[T]
}

SeqConfig configures optional sequence behavior

type SeqLazy

type SeqLazy[T Thing] interface {
	// Head lifts first element of sequence
	Head() (T, error)
	// Tail evaluates tail of sequence
	Tail() bool
	// Error returns error of stream evaluation
	Error() error
	// Cursor is the global position in the sequence
	Cursor() Thing
}

SeqLazy is an interface to iterate through collection of objects at storage

type Thing

type Thing interface {
	HashKey() curie.IRI
	SortKey() curie.IRI
}

Thing is the most generic item type used by the library to abstract writable/readable items into storage services.

The interfaces declares anything that have a unique identifier. The unique identity is exposed by pair of string: HashKey and SortKey.

type Things added in v1.5.0

type Things[T Thing] []T

Things is sequence of Thing

func (*Things[T]) Join added in v1.5.0

func (seq *Things[T]) Join(t T) error

Join lifts sequence of matched objects to seq of IDs

seq := dynamo.Things{}
dynamo.Match(...).FMap(seq.Join)

type TypeOf

type TypeOf[T Thing, A any] interface {
	Eq(A) Constraint[T]
	Ne(A) Constraint[T]
	Lt(A) Constraint[T]
	Le(A) Constraint[T]
	Gt(A) Constraint[T]
	Ge(A) Constraint[T]
	Is(string) Constraint[T]
	Exists() Constraint[T]
	NotExists() Constraint[T]
}

TypeOf declares type descriptor to express Storage I/O Constrains.

Let's consider a following example:

type Person struct {
  curie.ID
  Name    string `dynamodbav:"anothername,omitempty"`
}

How to define a condition expression on the field Name? Golang struct defines and refers the field by `Name` but DynamoDB stores it under the attribute `anothername`. Struct field dynamodbav tag specifies serialization rules. Golang does not support a typesafe approach to build a correspondence between `Name` ⟷ `anothername`. Developers have to utilize dynamodb attribute name(s) in conditional expression and Golang struct name in rest of the code. It becomes confusing and hard to maintain.

The types TypeOf and SchemaN are helpers to declare builders for conditional expressions. Just declare a global variables next to type definition and use them across the application.

  var name = dynamo.Schema1[Person, string]("Name")

	name.Eq("Joe Doe")
  name.NotExists()

func Schema1 added in v1.5.0

func Schema1[T Thing, A any](a string) TypeOf[T, A]

Schema1 builds Constrain builder for product type of arity 1

type URL added in v1.6.0

type URL url.URL

URL custom type with helper functions

func (*URL) Query added in v1.6.0

func (uri *URL) Query(key, def string) string

query parameters

func (*URL) Segments added in v1.6.0

func (uri *URL) Segments() []string

path segments of length

func (*URL) String added in v1.6.0

func (uri *URL) String() string

Directories

Path Synopsis
examples
internal
ddb
s3

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL