Skip to content
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

Cloud Firestore facade reference #701

Merged
merged 10 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
/references/data-api @danistrebel
/references/data-converters-shared-flow @tyayers
/references/dutch-healthcare @seymen
/references/firestore-facade @joelgauci
/references/gcp-sa-auth-shared-flow @danistrebel
/references/identity-facade @joelgauci
/references/java-callout @omidtahouri
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ further to fit a particular use case.
popular API auth schemes
- [reCAPTCHA enterprise](references/recaptcha-enterprise) - A reference for
API protection against bot leveraging reCAPTCHA enterprise
- [Firestore Facade](references/firestore-facade) - Reference implementation
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
for a storage/long term caching solution based on Firestore
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved

## Tools

Expand Down
3 changes: 3 additions & 0 deletions references/firestore-facade/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store
node_modules
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
AM-SetFirestoreMock.xml
158 changes: 158 additions & 0 deletions references/firestore-facade/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Firestore Facade in Apigee X/hybrid

[Cloud Firestore](https://firebase.google.com/docs/firestore)
is a flexible, scalable database for mobile, web, and server development
from Firebase and Google Cloud.
Apigee can act as a facade in front of Cloud Firestore, to implement the
following use cases:

- Long term storage: using Cloud Firestore to cache data on long term
- Data as a Service (DaaS) pattern: some data of a Cloud Firestore database
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
(db) are exposed as an API

The use case that is proposed in the Firestore facade reference is the one
based on long term storage.
Indeed, in situations where you need a caching mechanim for data,
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
which must be cached for more than 30 days (max TTL for caching data in
Apigee X) a storage solution is required.
Cloud Firestore is the perfect solution to consider in case of long
term storage needs.

## How it works?

Two Apigee **sharedflows** are used as a facade in front of a Cloud Firestore
db. A **key cache** is used to lookup and populate data into the Cloud
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
Firestore db.

By default, the key cache is defined as the following:
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved

```keyCache = base64Encoding(basePath + '/' + pathSuffix)```

This can be modified depending on the use case you need to implement

The 2 Shared Flows on Apigee, acting as a facade in front of a Cloud
Firestore db are:

- ```sf-firestore-facade-lookup-v1```: to lookup into a Cloud Firestore db
- ```sf-firestore-facade-populate-v1```: to populate a Cloud Firestore db
with backend responses

## Apigee runtime options

The Firestore facade reference can be deployed on both Apigee X and
hybrid. This reference would also work with Apigee Edge if the Service Account
token is obtained through the sharedflows, which invoke the Cloud Firestore
API endpoint (```sf-firestore-facade-lookup-v1``` and
```sf-firestore-facade-populate-v1```).

## Dependencies

- [Maven](https://maven.apache.org/)
- [NodeJS](https://nodejs.org/en/) LTS version or above
- An Apigee organization
- [Google Cloud Platform](https://cloud.google.com/) (GCP)

This reference leverages Apigee and Cloud Firestore.
Therefore, it is important to note that:

- A GCP service account is needed by the Apigee configuration
to securely invoke the Cloud Firestore API endpoint.
This service account is created during the deployment process on the GCP
project you are currently using: the ```pipeline.sh``` will attempt to create
a service account only if it doesn't exist.

In case you want to create this service account manually - or with Terraform,
please note that the role ```roles/firestore.admin``` must be granted
to it.

## Quick start

### Apigee X / hybrid

export APIGEE_X_ORG=xxx
export APIGEE_X_ENV=xxx
export APIGEE_X_HOSTNAME=xxx

./pipeline.sh

## Deployment options

There are 2 options to deploy the Firestore facade reference.

```Option 1``` is the default deployment option.

### Option 1: Cloud Firestore is mocked
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved

export IS_FIRESTORE_MOCK_ENABLED=true

With this option (*default*) no Cloud Firestore db is
invoked but a mock response similar to a real one is delivered.

**Functional tests are executed only when this deployment option is
selected**.

### Option 2: Cloud Firestore is used

export IS_FIRESTORE_MOCK_ENABLED=false

With this option, it is not possible to execute functional tests.
Nevertheless, you can request the Firestore facade API using the
HTTP client of your choice.
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved

## Script outputs

The pipeline script deploys on Apigee X / hybrid two
**sharedflows** (```sf-firestore-facade-lookup-v1```
and ```sf-firestore-facade-populate-v1```)
containing the full configuration of the Firestore
facade reference.

An API Proxy, acting as a data proxy, is also part of the reference:

- ```firestore-data-proxy-v1```: a data proxy, which calls the two
**sharedflows** accordingly.

The target endpoint of this proxy is [mocktarget.apigee.net/echo](https://mocktarget.apigee.net/echo)

## Cloud Firestore & Apigee Sequence Diagram

The following sequence diagram provides all the interactions between:

- End-user
- Client app
- Apigee: sharedflows and data proxy
- Cloud Firestore
- Backend

This sequence diagram is available as a
[text file](./diagram/sequence-firestore-facade.txt).

If needed, you can modify this file and re-generate the related picture (png)
using the following command:

./generate_docs.sh
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved

Here is the original sequence diagram:

![Firestore facade](./diagram/sequence-firestore-facade.png "Seq. Diagram")

## Testing the Firestore facade reference

In order to test this reference, you need an HTTP client.
If you execute your test using a real Cloud Firestore db, you have to create
it on your Google Cloud platform. You can use the ```(default)``` database.

### cURL command

Using cURL, the request is the following:

curl https://${APIGEE_X_HOSTNAME}/v1/firestore/users/123

Connect to your Cloud Firestore instance to check that data (response
of the Apigee Mock API) have been insterted into your Cloud Firestore db (
cf. **data** on the right side of the picture):

![default database](./images/cloud-firestore.png "default db in Cloud Firestore")

As you can notice, both ```basePath``` (**collectionId** in Cloud Firestore) and
```pathSuffix``` (**documentId** in Cloud Firestore) are base64 encoded.
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
@startuml
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved

title "Firestore Facade in Apigee X/hybrid"

actor User as u
entity "Client App" as b
entity "Cloud\nFirestore database" as gfd
box "Apigee API Platform" #LightBlue
entity "API Proxy\nfirestore-data-proxy-v1" as fdp
entity "SharedFlow\nsf-firestore-facade-lookup-v1" as ffl
entity "SharedFlow\nsf-firestore-facade-populate-v1" as ffp
end box
participant "Backend" as backend

u -> b: User interaction
b -> b: App activity
b -> fdp: Access the firestore facade api

note over gfd,ffp: "Apigee API proxy and shared flows acting as a facade in front of Cloud Firestore db"

fdp -> ffl: Lookup data from the Cloud Firestore db based on base path, path suffix and encoding type (base64 only)
ffl -> ffl: calculate the document key (key cache)\nkeyCache = encodingType( basePath + pathSuffix)
ffl -> gfd: Lookup shared flow acting as a facade with Cloud Firestore, using the key cache\ncall is executed using an ID token
gfd -> gfd: lookup in the Cloud Firestore db using key cache

opt Data retrieved from Cloud Firestore
gfd -> ffl: data is retrieved from Cloud Firestore (lookup status)
ffl -> ffl: set context variables:\nflow.lookup.iscontent.cached = true \nflow.lookup.content.cached = "<json content retrieved from cache>" \nflow.lookup.status.code = 200
ffl -> fdp: shared flow response
end

opt Data is NOT retrieved from Cloud Firestore
gfd -> ffl: data is not retrieved from Cloud Firestore (lookp status)
ffl -> ffl: set context variables:\nflow.lookup.iscontent.cached = false \nflow.lookup.content.cached = "none" \nflow.lookup.status.code = 404
ffl -> fdp: shared flow response
fdp -> backend: request is forwarded to the backend API
backend -> fdp: backend response
fdp -> ffp: Populate data from the Cloud Firestore db based on base path, path suffix and encoding type (base64 only)
ffp -> ffp: calculate the key cache\nkeyCache = encodingType( basePath + pathSuffix)
ffp -> gfd: Populate shared flow acting as a facade with Cloud Firestore, using the key cache\ncall is executed using an ID token
gfd -> gfd: populate backend response in the Firestore db using key cache
gfd -> ffp: firestore populate status
ffp -> ffp: set context variables:\nflow.populate.content.cached = true \nflow.populate.status.code = 200 \nflow.populate.keycache = <keycache> \nflow.populate.documentid = <firestore documentId> \nflow.populate.collectionid = <firestore collectionId>
ffp -> fdp: shared flow response

end

fdp -> b: JSON response is sent back to the app (200 OK)

@enduml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright 2023 Google LLC
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.
-->
<APIProxy revision="1" name="firestore-data-proxy-v1"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright 2023 Google LLC
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.
-->
<AssignMessage name="AM-PathSuffixFalse">
<AssignVariable>
<Name>target.copy.pathsuffix</Name>
<Value>false</Value>
</AssignVariable>
<AssignTo createNew="false" transport="http" type="request"/>
</AssignMessage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright 2023 Google LLC
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.
-->
<AssignMessage name="AM-SetResponse">
<AssignVariable>
<Name>message.content</Name>
<Ref>flow.lookup.content.cached</Ref>
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
</AssignVariable>
<AssignVariable>
<Name>message.header.content-type</Name>
<Value>application/json</Value>
</AssignVariable>
<IgnoreUnresolvedVariables>true</IgnoreUnresolvedVariables>
<AssignTo createNew="false" transport="http" type="response"/>
</AssignMessage>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright 2023 Google LLC
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.
-->
<FlowCallout name="FC-LookupExternalCache">
<Parameters>
<Parameter name="encodingType">base64</Parameter>
<Parameter name="basePath">{proxy.basepath}</Parameter>
<Parameter name="pathSuffix">{proxy.pathsuffix}</Parameter>
</Parameters>
<SharedFlowBundle>sf-firestore-facade-lookup-v1</SharedFlowBundle>
</FlowCallout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright 2023 Google LLC
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.
-->
<FlowCallout name="FC-SetContentInCache">
<Parameters>
<Parameter name="encodedbasePath">{flow.basePath}</Parameter>
<Parameter name="encodedpathSuffix">{flow.pathSuffix}</Parameter>
<Parameter name="jsonContentAsString">{response.content}</Parameter>
</Parameters>
<SharedFlowBundle>sf-firestore-facade-populate-v1</SharedFlowBundle>
</FlowCallout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!--
Copyright 2023 Google LLC
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.
-->
<ProxyEndpoint name="default">
<PreFlow name="PreFlow">
<Request>
<Step>
<Name>FC-LookupExternalCache</Name>
</Step>
<Step>
<Condition>flow.lookup.iscontent.cached = "true"</Condition>
<Name>AM-SetResponse</Name>
</Step>
</Request>
<Response>
<Step>
<Condition>(flow.lookup.iscontent.cached = "false") and (flow.lookup.status.code = 404)</Condition>
JoelGauci marked this conversation as resolved.
Show resolved Hide resolved
<Name>FC-PopulateExternalCache</Name>
</Step>
</Response>
</PreFlow>
<Flows/>
<PostFlow name="PostFlow">
<Request/>
<Response/>
</PostFlow>
<HTTPProxyConnection>
<BasePath>/v1/firestore</BasePath>
</HTTPProxyConnection>
<RouteRule name="noroute">
<Condition>flow.lookup.iscontent.cached = "true"</Condition>
</RouteRule>
<RouteRule name="default">
<TargetEndpoint>default</TargetEndpoint>
</RouteRule>
</ProxyEndpoint>
Loading
Loading