1// Copyright 2019 The Go Cloud Development Kit Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package awsdynamodb
16
17import (
18	"context"
19	"errors"
20	"fmt"
21	"net/url"
22	"sync"
23
24	"github.com/aws/aws-sdk-go/aws/client"
25	dyn "github.com/aws/aws-sdk-go/service/dynamodb"
26	gcaws "gocloud.dev/aws"
27	"gocloud.dev/docstore"
28)
29
30func init() {
31	docstore.DefaultURLMux().RegisterCollection(Scheme, new(lazySessionOpener))
32}
33
34type lazySessionOpener struct {
35	init   sync.Once
36	opener *URLOpener
37	err    error
38}
39
40func (o *lazySessionOpener) OpenCollectionURL(ctx context.Context, u *url.URL) (*docstore.Collection, error) {
41	o.init.Do(func() {
42		sess, err := gcaws.NewDefaultSession()
43		if err != nil {
44			o.err = err
45			return
46		}
47		o.opener = &URLOpener{
48			ConfigProvider: sess,
49		}
50	})
51	if o.err != nil {
52		return nil, fmt.Errorf("open collection %s: %v", u, o.err)
53	}
54	return o.opener.OpenCollectionURL(ctx, u)
55}
56
57// Scheme is the URL scheme dynamodb registers its URLOpener under on
58// docstore.DefaultMux.
59const Scheme = "dynamodb"
60
61// URLOpener opens dynamodb URLs like
62// "dynamodb://mytable?partition_key=partkey&sort_key=sortkey".
63//
64// The URL Host is used as the table name. See
65// https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
66// for more details.
67//
68// The following query parameters are supported:
69//
70//   - partition_key (required): the path to the partition key of a table or an index.
71//   - sort_key: the path to the sort key of a table or an index.
72//   - allow_scans: if "true", allow table scans to be used for queries
73//
74// See https://godoc.org/gocloud.dev/aws#ConfigFromURLParams for supported query
75// parameters for overriding the aws.Session from the URL.
76type URLOpener struct {
77	// ConfigProvider must be set to a non-nil value.
78	ConfigProvider client.ConfigProvider
79}
80
81// OpenCollectionURL opens the collection at the URL's path. See the package doc for more details.
82func (o *URLOpener) OpenCollectionURL(_ context.Context, u *url.URL) (*docstore.Collection, error) {
83	db, tableName, partitionKey, sortKey, opts, err := o.processURL(u)
84	if err != nil {
85		return nil, err
86	}
87	return OpenCollection(db, tableName, partitionKey, sortKey, opts)
88}
89
90func (o *URLOpener) processURL(u *url.URL) (db *dyn.DynamoDB, tableName, partitionKey, sortKey string, opts *Options, err error) {
91	q := u.Query()
92
93	partitionKey = q.Get("partition_key")
94	if partitionKey == "" {
95		return nil, "", "", "", nil, fmt.Errorf("open collection %s: partition_key is required to open a table", u)
96	}
97	q.Del("partition_key")
98	sortKey = q.Get("sort_key")
99	q.Del("sort_key")
100	opts = &Options{
101		AllowScans:    q.Get("allow_scans") == "true",
102		RevisionField: q.Get("revision_field"),
103	}
104	q.Del("allow_scans")
105	q.Del("revision_field")
106
107	tableName = u.Host
108	if tableName == "" {
109		return nil, "", "", "", nil, fmt.Errorf("open collection %s: URL's host cannot be empty (the table name)", u)
110	}
111	if u.Path != "" {
112		return nil, "", "", "", nil, fmt.Errorf("open collection %s: URL path must be empty, only the host is needed", u)
113	}
114
115	configProvider := &gcaws.ConfigOverrider{
116		Base: o.ConfigProvider,
117	}
118	overrideCfg, err := gcaws.ConfigFromURLParams(q)
119	if err != nil {
120		return nil, "", "", "", nil, fmt.Errorf("open collection %s: %v", u, err)
121	}
122	configProvider.Configs = append(configProvider.Configs, overrideCfg)
123	db, err = Dial(configProvider)
124	if err != nil {
125		return nil, "", "", "", nil, fmt.Errorf("open collection %s: %v", u, err)
126	}
127	return db, tableName, partitionKey, sortKey, opts, nil
128}
129
130// Dial gets an AWS DynamoDB service client.
131func Dial(p client.ConfigProvider) (*dyn.DynamoDB, error) {
132	if p == nil {
133		return nil, errors.New("getting Dynamo service: no AWS session provided")
134	}
135	return dyn.New(p), nil
136}
137