├── .github └── FUNDING.yml ├── .gitignore ├── Readme.md ├── news.go ├── news_example_test.go └── news_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: tj -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # News 2 | 3 | A minimal DynamoDB-backed mailing list package for Go. 4 | 5 | ## Setup 6 | 7 | Create a DynamoDB table with a __Partition Key__ of "newsletter", and a __Sort Key__ of "email". 8 | 9 | ## Links 10 | 11 | Check out the [news-api](https://github.com/tj/news-api) for an HTTP API handling subscriptions. 12 | 13 | --- 14 | 15 | [![GoDoc](https://godoc.org/github.com/tj/go-news?status.svg)](https://godoc.org/github.com/tj/go-news) 16 | ![](https://img.shields.io/badge/license-MIT-blue.svg) 17 | ![](https://img.shields.io/badge/status-stable-green.svg) 18 | 19 | 20 | -------------------------------------------------------------------------------- /news.go: -------------------------------------------------------------------------------- 1 | // Package news provides a very simple DynamoDB-backed mailing list for newsletters. 2 | package news 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/aws/aws-sdk-go/aws" 8 | "github.com/aws/aws-sdk-go/aws/session" 9 | "github.com/aws/aws-sdk-go/service/dynamodb" 10 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute" 11 | "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface" 12 | ) 13 | 14 | // item model. 15 | type item struct { 16 | Newsletter string `json:"newsletter"` 17 | Email string `json:"email"` 18 | CreatedAt time.Time `json:"created_at"` 19 | } 20 | 21 | // New returns a new mailing list store with default AWS credentials. 22 | func New(table string) *Store { 23 | return &Store{ 24 | Client: dynamodb.New(session.New(aws.NewConfig())), 25 | TableName: table, 26 | } 27 | } 28 | 29 | // Store is a DynamoDB mailing list storage implementation. 30 | type Store struct { 31 | TableName string 32 | Client dynamodbiface.DynamoDBAPI 33 | } 34 | 35 | // AddSubscriber adds a subscriber to a newsletter. 36 | func (s *Store) AddSubscriber(newsletter, email string) error { 37 | i, err := dynamodbattribute.MarshalMap(item{ 38 | Newsletter: newsletter, 39 | Email: email, 40 | CreatedAt: time.Now(), 41 | }) 42 | 43 | if err != nil { 44 | return err 45 | } 46 | 47 | _, err = s.Client.PutItem(&dynamodb.PutItemInput{ 48 | TableName: &s.TableName, 49 | Item: i, 50 | }) 51 | 52 | if err != nil { 53 | return err 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // RemoveSubscriber removes a subscriber from a newsletter. 60 | func (s *Store) RemoveSubscriber(newsletter, email string) error { 61 | _, err := s.Client.DeleteItem(&dynamodb.DeleteItemInput{ 62 | TableName: &s.TableName, 63 | Key: map[string]*dynamodb.AttributeValue{ 64 | "newsletter": &dynamodb.AttributeValue{ 65 | S: &newsletter, 66 | }, 67 | "email": &dynamodb.AttributeValue{ 68 | S: &email, 69 | }, 70 | }, 71 | }) 72 | 73 | return err 74 | } 75 | 76 | // GetSubscribers returns subscriber emails for a newsletter. 77 | func (s *Store) GetSubscribers(newsletter string) (emails []string, err error) { 78 | query := &dynamodb.QueryInput{ 79 | TableName: &s.TableName, 80 | KeyConditionExpression: aws.String(`newsletter = :newsletter`), 81 | ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ 82 | ":newsletter": &dynamodb.AttributeValue{ 83 | S: &newsletter, 84 | }, 85 | }, 86 | } 87 | 88 | err = s.Client.QueryPages(query, func(page *dynamodb.QueryOutput, more bool) bool { 89 | for _, item := range page.Items { 90 | if v, ok := item["email"]; ok { 91 | emails = append(emails, *v.S) 92 | } 93 | } 94 | return true 95 | }) 96 | 97 | return 98 | } 99 | -------------------------------------------------------------------------------- /news_example_test.go: -------------------------------------------------------------------------------- 1 | package news_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/tj/go-news" 8 | ) 9 | 10 | func Example() { 11 | list := news.New("news_test") 12 | 13 | emails := []string{ 14 | "tobi@apex.sh", 15 | "loki@apex.sh", 16 | "jane@apex.sh", 17 | "manny@apex.sh", 18 | "luna@apex.sh", 19 | } 20 | 21 | for _, email := range emails { 22 | err := list.AddSubscriber("product_updates", email) 23 | if err != nil { 24 | log.Fatalf("error: %s\n", err) 25 | } 26 | } 27 | 28 | subscribers, err := list.GetSubscribers("product_updates") 29 | if err != nil { 30 | log.Fatalf("error: %s\n", err) 31 | } 32 | 33 | fmt.Printf("%#v\n", subscribers) 34 | // Output: 35 | // []string{"jane@apex.sh", "loki@apex.sh", "luna@apex.sh", "manny@apex.sh", "tobi@apex.sh"} 36 | } 37 | -------------------------------------------------------------------------------- /news_test.go: -------------------------------------------------------------------------------- 1 | package news_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tj/assert" 7 | 8 | "github.com/tj/go-news" 9 | ) 10 | 11 | // newStorage helper. 12 | func newStorage(t testing.TB) *news.Store { 13 | return news.New("news_test") 14 | } 15 | 16 | // Test adding subscribers. 17 | func TestStore_AddSubscriber(t *testing.T) { 18 | db := newStorage(t) 19 | 20 | err := db.AddSubscriber("general", "manny@apex.sh") 21 | assert.NoError(t, err, "adding subscriber") 22 | 23 | err = db.AddSubscriber("general", "tobi@apex.sh") 24 | assert.NoError(t, err, "adding subscriber") 25 | 26 | err = db.AddSubscriber("general", "loki@apex.sh") 27 | assert.NoError(t, err, "adding subscriber") 28 | 29 | err = db.AddSubscriber("general", "jane@apex.sh") 30 | assert.NoError(t, err, "adding subscriber") 31 | 32 | err = db.AddSubscriber("blog", "tj@apex.sh") 33 | assert.NoError(t, err, "adding subscriber") 34 | 35 | err = db.AddSubscriber("blog", "jane@apex.sh") 36 | assert.NoError(t, err, "adding subscriber") 37 | } 38 | 39 | // Test removing subscribers. 40 | func TestStore_RemoveSubscriber(t *testing.T) { 41 | db := newStorage(t) 42 | 43 | err := db.AddSubscriber("up", "luna@apex.sh") 44 | assert.NoError(t, err, "adding subscriber") 45 | 46 | emails, err := db.GetSubscribers("up") 47 | assert.NoError(t, err, "listing subscribers") 48 | assert.Len(t, emails, 1, "emails") 49 | 50 | err = db.RemoveSubscriber("up", "luna@apex.sh") 51 | assert.NoError(t, err, "removing subscriber") 52 | 53 | emails, err = db.GetSubscribers("up") 54 | assert.NoError(t, err, "listing subscribers") 55 | assert.Len(t, emails, 0, "emails") 56 | 57 | err = db.RemoveSubscriber("up", "luna@apex.sh") 58 | assert.NoError(t, err, "removing subscriber") 59 | } 60 | 61 | // Test listing subscribers. 62 | func TestStore_GetSubscribers(t *testing.T) { 63 | db := newStorage(t) 64 | 65 | emails, err := db.GetSubscribers("general") 66 | assert.NoError(t, err, "listing subscribers") 67 | assert.Len(t, emails, 4, "emails") 68 | 69 | emails, err = db.GetSubscribers("blog") 70 | assert.NoError(t, err, "listing subscribers") 71 | assert.Len(t, emails, 2, "emails") 72 | } 73 | --------------------------------------------------------------------------------