-
Notifications
You must be signed in to change notification settings - Fork 271
feat(rtdb): add go rtdb emulator support #517
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 24 commits
54b8114
cef91ac
77177c7
a957589
eb0d2a0
05378ef
4121c50
928b104
02cde4f
6b40682
e60757f
bb055ed
23a1f17
21d7d61
fda4e90
2d92931
12e6a60
05f172b
3d95aa6
9cce22e
dcac128
e3e2077
1e8716c
18519f1
ece67c0
d611de6
22b76d3
c0078c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,36 +18,56 @@ package db | |
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "net/url" | ||
| "os" | ||
| "runtime" | ||
| "strings" | ||
|
|
||
| "firebase.google.com/go/v4/internal" | ||
| "golang.org/x/oauth2" | ||
| "google.golang.org/api/option" | ||
| ) | ||
|
|
||
| const userAgentFormat = "Firebase/HTTP/%s/%s/AdminGo" | ||
| const invalidChars = "[].#$" | ||
| const authVarOverride = "auth_variable_override" | ||
| const emulatorDatabaseEnvVar = "FIREBASE_DATABASE_EMULATOR_HOST" | ||
| const emulatorNamespaceParam = "ns" | ||
|
|
||
| var ErrInvalidURL = errors.New("invalid database url") | ||
|
|
||
| var emulatorToken = &oauth2.Token{ | ||
| AccessToken: "owner", | ||
| } | ||
|
|
||
| // Client is the interface for the Firebase Realtime Database service. | ||
| type Client struct { | ||
| hc *internal.HTTPClient | ||
| url string | ||
| dbURLConfig *dbURLConfig | ||
| authOverride string | ||
| } | ||
|
|
||
| type dbURLConfig struct { | ||
| // BaseURL can be either: | ||
| // - a production url (https://foo-bar.firebaseio.com/) | ||
| // - an emulator url (http://localhost:9000) | ||
| BaseURL string | ||
|
|
||
| // Namespace is used in for the emulator to specify the databaseName | ||
| // To specify a namespace on your url, pass ns=<database_name> (localhost:9000/?ns=foo-bar) | ||
| Namespace string | ||
| } | ||
|
|
||
| // NewClient creates a new instance of the Firebase Database Client. | ||
| // | ||
| // This function can only be invoked from within the SDK. Client applications should access the | ||
| // Database service through firebase.App. | ||
| func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error) { | ||
| p, err := url.ParseRequestURI(c.URL) | ||
| urlConfig, isEmulator, err := parseURLConfig(c.URL) | ||
| if err != nil { | ||
| return nil, err | ||
| } else if p.Scheme != "https" { | ||
| return nil, fmt.Errorf("invalid database URL: %q; want scheme: %q", c.URL, "https") | ||
| } | ||
|
|
||
| var ao []byte | ||
|
|
@@ -59,6 +79,10 @@ func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error) | |
| } | ||
|
|
||
| opts := append([]option.ClientOption{}, c.Opts...) | ||
| if isEmulator { | ||
| ts := oauth2.StaticTokenSource(emulatorToken) | ||
| opts = append(opts, option.WithTokenSource(ts)) | ||
| } | ||
| ua := fmt.Sprintf(userAgentFormat, c.Version, runtime.Version()) | ||
| opts = append(opts, option.WithUserAgent(ua)) | ||
| hc, _, err := internal.NewHTTPClient(ctx, opts...) | ||
|
|
@@ -69,7 +93,7 @@ func NewClient(ctx context.Context, c *internal.DatabaseConfig) (*Client, error) | |
| hc.CreateErrFn = handleRTDBError | ||
| return &Client{ | ||
| hc: hc, | ||
| url: fmt.Sprintf("https://%s", p.Host), | ||
| dbURLConfig: urlConfig, | ||
| authOverride: string(ao), | ||
| }, nil | ||
| } | ||
|
|
@@ -96,10 +120,13 @@ func (c *Client) sendAndUnmarshal( | |
| return nil, fmt.Errorf("invalid path with illegal characters: %q", req.URL) | ||
| } | ||
|
|
||
| req.URL = fmt.Sprintf("%s%s.json", c.url, req.URL) | ||
| req.URL = fmt.Sprintf("%s%s.json", c.dbURLConfig.BaseURL, req.URL) | ||
| if c.authOverride != "" { | ||
| req.Opts = append(req.Opts, internal.WithQueryParam(authVarOverride, c.authOverride)) | ||
| } | ||
| if c.dbURLConfig.Namespace != "" { | ||
| req.Opts = append(req.Opts, internal.WithQueryParam(emulatorNamespaceParam, c.dbURLConfig.Namespace)) | ||
| } | ||
|
|
||
| return c.hc.DoAndUnmarshal(ctx, req, v) | ||
| } | ||
|
|
@@ -126,3 +153,65 @@ func handleRTDBError(resp *internal.Response) error { | |
|
|
||
| return err | ||
| } | ||
|
|
||
| // parseURLConfig returns the dbURLConfig for the database | ||
| // dbURL may be either: | ||
| // - a production url (https://foo-bar.firebaseio.com/) | ||
| // - an emulator URL (localhost:9000/?ns=foo-bar) | ||
| // | ||
| // The following rules will apply for determining the output: | ||
| // - If the url has no scheme it will be assumed to be an emulator url and be used. | ||
| // - else If the FIREBASE_DATABASE_EMULATOR_HOST environment variable is set it will be used. | ||
| // - else the url will be assumed to be a production url and be used. | ||
| func parseURLConfig(dbURL string) (*dbURLConfig, bool, error) { | ||
| parsedURL, err := url.ParseRequestURI(dbURL) | ||
| if err == nil && parsedURL.Scheme != "https" { | ||
| cfg, err := parseEmulatorHost(dbURL, parsedURL) | ||
| return cfg, true, err | ||
| } | ||
|
|
||
| environmentEmulatorURL := os.Getenv(emulatorDatabaseEnvVar) | ||
| if environmentEmulatorURL == "" && err != nil { | ||
| return nil, false, fmt.Errorf("%s: %w", dbURL, ErrInvalidURL) | ||
| } | ||
|
|
||
| if environmentEmulatorURL == "" && err == nil { | ||
| return &dbURLConfig{ | ||
| BaseURL: dbURL, | ||
| Namespace: "", | ||
| }, false, nil | ||
| } | ||
|
|
||
| parsedURL, err = url.ParseRequestURI(environmentEmulatorURL) | ||
| if err != nil { | ||
| return nil, false, fmt.Errorf("%s: %w", environmentEmulatorURL, ErrInvalidURL) | ||
| } | ||
| cfg, err := parseEmulatorHost(environmentEmulatorURL, parsedURL) | ||
| return cfg, true, err | ||
| } | ||
|
|
||
| func parseEmulatorHost(rawEmulatorHostURL string, parsedEmulatorHost *url.URL) (*dbURLConfig, error) { | ||
| if strings.Contains(rawEmulatorHostURL, "//") { | ||
|
Comment on lines
+195
to
+196
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 saw this behavior in the Python sdk: https://github.com/firebase/firebase-admin-python/blob/37ecf18d0ad09d7402143f22443e957a57ec7a2a/firebase_admin/db.py#L784 |
||
| return nil, fmt.Errorf(`invalid %s: "%s". It must follow format "host:port": %w`, emulatorDatabaseEnvVar, rawEmulatorHostURL, ErrInvalidURL) | ||
| } | ||
|
|
||
| baseURL := strings.Replace(rawEmulatorHostURL, fmt.Sprintf("?%s", parsedEmulatorHost.RawQuery), "", -1) | ||
| if parsedEmulatorHost.Scheme != "http" { | ||
| baseURL = fmt.Sprintf("http://%s", baseURL) | ||
| } | ||
|
|
||
| namespace := parsedEmulatorHost.Query().Get(emulatorNamespaceParam) | ||
| if namespace == "" { | ||
| if strings.Contains(rawEmulatorHostURL, ".") { | ||
| namespace = strings.Split(rawEmulatorHostURL, ".")[0] | ||
| } | ||
|
Comment on lines
+207
to
+209
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 observed similar behaviour in the Node sdk: https://github.com/firebase/firebase-admin-node/blob/master/src/database/database.ts#L325 |
||
| if namespace == "" { | ||
| return nil, fmt.Errorf(`invalid database URL: "%s". Database URL must be a valid URL to a Firebase Realtime Database instance (include ?ns=<db-name> query param)`, parsedEmulatorHost) | ||
| } | ||
| } | ||
|
|
||
| return &dbURLConfig{ | ||
| BaseURL: baseURL, | ||
| Namespace: namespace, | ||
| }, nil | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.