-
Notifications
You must be signed in to change notification settings - Fork 270
Experiment: FDC ExecuteGraphql and ExecuteGraphqlRead
#716
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
Draft
jonathanedey
wants to merge
9
commits into
dev
Choose a base branch
from
jules-fdc-client-1
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
7edaf14
feat(fdc): Add Firebase Data Connect service
google-labs-jules[bot] 8984b88
fix(fdc): Refactor Dataconnect types into and update query error code
jonathanedey a40c03f
chore(fdc): Add integration tests
jonathanedey 0e14758
fix(fdc): Simplify error logic
jonathanedey 7bcdb31
fix(fdc): Fixed lint
jonathanedey 756ff9a
chore: Fix copyright headers
jonathanedey 99644df
chore: Add updated schema
jonathanedey f91b4c0
feat: Allow passing of an interface to unmarshal custom responses and…
jonathanedey dcac8f9
fix: lint
jonathanedey File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,190 @@ | ||
| // Copyright 2025 Google Inc. All Rights Reserved. | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| // Package dataconnect contains functions for interacting with the Firebase Data Connect service. | ||
| package dataconnect | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/http" | ||
| "os" | ||
|
|
||
| "firebase.google.com/go/v4/internal" | ||
| "google.golang.org/api/option" | ||
| "google.golang.org/api/transport" | ||
| ) | ||
|
|
||
| const ( | ||
| dataConnectProdURLFormat = "https://firebasedataconnect.googleapis.com/%s/projects/%s/locations/%s/services/%s:%s" | ||
| dataConnectEmulatorURLFormat = "http://%s/%s/projects/%s/locations/%s/services/%s:%s" | ||
| emulatorHostEnvVar = "FIREBASE_DATA_CONNECT_EMULATOR_HOST" | ||
| apiVersion = "v1alpha" | ||
| executeGraphqlEndpoint = "executeGraphql" | ||
| executeGraphqlReadEndpoint = "executeGraphqlRead" | ||
|
|
||
| // SDK-generated error codes | ||
| queryError = "QUERY_ERROR" | ||
| ) | ||
|
|
||
| // ConnectorConfig is the configuration for the Data Connect service. | ||
| type ConnectorConfig struct { | ||
| Location string `json:"location"` | ||
| ServiceID string `json:"serviceId"` | ||
| } | ||
|
|
||
| // GraphqlOptions represents the options for a GraphQL query. | ||
| type GraphqlOptions struct { | ||
| Variables interface{} `json:"variables,omitempty"` | ||
| OperationName string `json:"operationName,omitempty"` | ||
| } | ||
|
|
||
| // ExecuteGraphqlResponse is the response from a GraphQL query. | ||
| type internalExecuteGraphqlResponse struct { | ||
| Data json.RawMessage `json:"data"` | ||
| } | ||
|
|
||
| // Client is the interface for the Firebase Data Connect service. | ||
| type Client struct { | ||
| client *internal.HTTPClient | ||
| projectID string | ||
| location string | ||
| serviceID string | ||
| isEmulator bool | ||
| emulatorHost string | ||
| } | ||
|
|
||
| // NewClient creates a new instance of the Data Connect client. | ||
| // | ||
| // This function can only be invoked from within the SDK. Client applications should access the | ||
| // Data Connect service through firebase.App. | ||
| func NewClient(ctx context.Context, conf *internal.DataConnectConfig) (*Client, error) { | ||
| var opts []option.ClientOption | ||
| opts = append(opts, conf.Opts...) | ||
|
|
||
| var isEmulator bool | ||
| emulatorHost := os.Getenv(emulatorHostEnvVar) | ||
| if emulatorHost != "" { | ||
| isEmulator = true | ||
| } | ||
|
|
||
| transport, _, err := transport.NewHTTPClient(ctx, opts...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| hc := internal.WithDefaultRetryConfig(transport) | ||
| hc.CreateErrFn = handleError | ||
| hc.SuccessFn = func(r *internal.Response) bool { | ||
| // If the status isn't already a know success status we handle these responses normally | ||
| if !internal.HasSuccessStatus(r) { | ||
| return false | ||
| } | ||
| // Otherwise we check the successful response body for error | ||
| var errResp graphqlQueryErrorResponse | ||
| if err := json.Unmarshal(r.Body, &errResp); err != nil { | ||
| return true // Cannot parse, assume no query errors and thus success | ||
| } | ||
| return len(errResp.Errors) == 0 | ||
| } | ||
| hc.Opts = []internal.HTTPOption{ | ||
| internal.WithHeader("X-Client-Version", fmt.Sprintf("Go/Admin/%s", conf.Version)), | ||
| internal.WithHeader("x-goog-api-client", internal.GetMetricsHeader(conf.Version)), | ||
| } | ||
|
|
||
| return &Client{ | ||
| client: hc, | ||
| projectID: conf.ProjectID, | ||
| location: conf.Location, | ||
| serviceID: conf.ServiceID, | ||
| isEmulator: isEmulator, | ||
| emulatorHost: emulatorHost, | ||
| }, nil | ||
| } | ||
|
|
||
| // ExecuteGraphql executes a GraphQL query or mutation. | ||
| func (c *Client) ExecuteGraphql(ctx context.Context, query string, options *GraphqlOptions, response interface{}) error { | ||
| return c.execute(ctx, executeGraphqlEndpoint, query, options, response) | ||
| } | ||
|
|
||
| // ExecuteGraphqlRead executes a GraphQL read-only query. | ||
| func (c *Client) ExecuteGraphqlRead(ctx context.Context, query string, options *GraphqlOptions, response interface{}) error { | ||
| return c.execute(ctx, executeGraphqlReadEndpoint, query, options, response) | ||
| } | ||
|
|
||
| func (c *Client) execute(ctx context.Context, endpoint, query string, options *GraphqlOptions, response interface{}) error { | ||
| url := c.buildURL(endpoint) | ||
|
|
||
| req := map[string]interface{}{ | ||
| "query": query, | ||
| } | ||
| if options != nil { | ||
| if options.Variables != nil { | ||
| req["variables"] = options.Variables | ||
| } | ||
| if options.OperationName != "" { | ||
| req["operationName"] = options.OperationName | ||
| } | ||
| } | ||
|
|
||
| var result internalExecuteGraphqlResponse | ||
| request := &internal.Request{ | ||
| Method: http.MethodPost, | ||
| URL: url, | ||
| Body: internal.NewJSONEntity(req), | ||
| } | ||
| _, err := c.client.DoAndUnmarshal(ctx, request, &result) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if response != nil { | ||
| if err := json.Unmarshal(result.Data, &response); err != nil { | ||
| return fmt.Errorf("error while parsing response: %v", err) | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func (c *Client) buildURL(endpoint string) string { | ||
| if c.isEmulator { | ||
| return fmt.Sprintf(dataConnectEmulatorURLFormat, c.emulatorHost, apiVersion, c.projectID, c.location, c.serviceID, endpoint) | ||
| } | ||
| return fmt.Sprintf(dataConnectProdURLFormat, apiVersion, c.projectID, c.location, c.serviceID, endpoint) | ||
| } | ||
|
|
||
| type graphqlQueryErrorResponse struct { | ||
| Errors []map[string]interface{} `json:"errors"` | ||
| } | ||
|
|
||
| func handleError(resp *internal.Response) error { | ||
| fe := internal.NewFirebaseError(resp) | ||
| var errResp graphqlQueryErrorResponse | ||
| if err := json.Unmarshal(resp.Body, &errResp); err == nil && len(errResp.Errors) > 0 { | ||
| // Unmarshalling here verifies query error exists | ||
| fe.ErrorCode = queryError | ||
| } | ||
| return fe | ||
| } | ||
|
|
||
| // IsQueryError checks if the given error is a query error. | ||
| func IsQueryError(err error) bool { | ||
| fe, ok := err.(*internal.FirebaseError) | ||
| if !ok { | ||
| return false | ||
| } | ||
|
|
||
| return fe.ErrorCode == queryError | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
When returning an error that wraps another error, it's a Go best practice to use
fmt.Errorfwith the%wverb. This preserves the original error, allowing callers to inspect it usingerrors.Isorerrors.As. This can be very helpful for debugging and for programmatic error handling by users of the SDK.