244 lines
9.2 KiB
GDScript
244 lines
9.2 KiB
GDScript
## @meta-authors Nicolò 'fenix' Santilio,
|
|
## @meta-version 2.5
|
|
##
|
|
## Referenced by [code]Firebase.Firestore[/code]. Represents the Firestore module.
|
|
## Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud.
|
|
## Like Firebase Realtime Database, it keeps your data in sync across client apps through realtime listeners and offers offline support for mobile and web so you can build responsive apps that work regardless of network latency or Internet connectivity. Cloud Firestore also offers seamless integration with other Firebase and Google Cloud products, including Cloud Functions.
|
|
##
|
|
## Following Cloud Firestore's NoSQL data model, you store data in [b]documents[/b] that contain fields mapping to values. These documents are stored in [b]collections[/b], which are containers for your documents that you can use to organize your data and build queries.
|
|
## Documents support many different data types, from simple strings and numbers, to complex, nested objects. You can also create subcollections within documents and build hierarchical data structures that scale as your database grows.
|
|
## The Cloud Firestore data model supports whatever data structure works best for your app.
|
|
##
|
|
## (source: [url=https://firebase.google.com/docs/firestore]Firestore[/url])
|
|
##
|
|
## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore
|
|
@tool
|
|
class_name FirebaseFirestore
|
|
extends Node
|
|
|
|
const _API_VERSION : String = "v1"
|
|
|
|
## Emitted when a [code]list()[/code] or [code]query()[/code] request is [b]not[/b] successfully completed.
|
|
signal error(code, status, message)
|
|
|
|
enum Requests {
|
|
NONE = -1, ## Firestore is not processing any request.
|
|
LIST, ## Firestore is processing a [code]list()[/code] request checked a collection.
|
|
QUERY ## Firestore is processing a [code]query()[/code] request checked a collection.
|
|
}
|
|
|
|
# TODO: Implement cache size limit
|
|
const CACHE_SIZE_UNLIMITED = -1
|
|
|
|
const _CACHE_EXTENSION : String = ".fscache"
|
|
const _CACHE_RECORD_FILE : String = "RmlyZXN0b3JlIGNhY2hlLXJlY29yZHMu.fscache"
|
|
|
|
const _AUTHORIZATION_HEADER : String = "Authorization: Bearer "
|
|
|
|
const _MAX_POOLED_REQUEST_AGE = 30
|
|
|
|
## The code indicating the request Firestore is processing.
|
|
## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers.
|
|
## @enum Requests
|
|
var request: int = -1
|
|
|
|
## A Dictionary containing all authentication fields for the current logged user.
|
|
## @type Dictionary
|
|
var auth: Dictionary
|
|
|
|
var _config: Dictionary = {}
|
|
var _cache_loc: String
|
|
var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id()
|
|
|
|
|
|
var _base_url: String = ""
|
|
var _extended_url: String = "projects/[PROJECT_ID]/databases/(default)/documents/"
|
|
var _query_suffix: String = ":runQuery"
|
|
var _agg_query_suffix: String = ":runAggregationQuery"
|
|
|
|
#var _connect_check_node : HTTPRequest
|
|
|
|
var _request_list_node: HTTPRequest
|
|
var _requests_queue: Array = []
|
|
var _current_query: FirestoreQuery
|
|
|
|
## Returns a reference collection by its [i]path[/i].
|
|
##
|
|
## The returned object will be of [code]FirestoreCollection[/code] type.
|
|
## If saved into a variable, it can be used to issue requests checked the collection itself.
|
|
## @args path
|
|
## @return FirestoreCollection
|
|
func collection(path : String) -> FirestoreCollection:
|
|
for coll in get_children():
|
|
if coll is FirestoreCollection:
|
|
if coll.collection_name == path:
|
|
return coll
|
|
|
|
var coll : FirestoreCollection = FirestoreCollection.new()
|
|
coll._extended_url = _extended_url
|
|
coll._base_url = _base_url
|
|
coll._config = _config
|
|
coll.auth = auth
|
|
coll.collection_name = path
|
|
add_child(coll)
|
|
return coll
|
|
|
|
|
|
## Issue a query checked your Firestore database.
|
|
##
|
|
## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query.
|
|
## When awaited, this function returns the resulting array from the query.
|
|
##
|
|
## ex.
|
|
## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code]
|
|
##
|
|
## [b]Warning:[/b] It currently does not work offline!
|
|
##
|
|
## @args query
|
|
## @arg-types FirestoreQuery
|
|
## @return Array[FirestoreDocument]
|
|
func query(query : FirestoreQuery) -> Array:
|
|
if query.aggregations.size() > 0:
|
|
Firebase._printerr("Aggregation query sent with normal query call: " + str(query))
|
|
return []
|
|
|
|
var task : FirestoreTask = FirestoreTask.new()
|
|
task.action = FirestoreTask.Task.TASK_QUERY
|
|
var body: Dictionary = { structuredQuery = query.query }
|
|
var url: String = _base_url + _extended_url + query.sub_collection_path + _query_suffix
|
|
|
|
task.data = query
|
|
task._fields = JSON.stringify(body)
|
|
task._url = url
|
|
_pooled_request(task)
|
|
return await _handle_task_finished(task)
|
|
|
|
## Issue an aggregation query (sum, average, count) against your Firestore database;
|
|
## cheaper than a normal query and counting (for instance) values directly.
|
|
##
|
|
## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query.
|
|
## When awaited, this function returns the result from the aggregation query.
|
|
##
|
|
## ex.
|
|
## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code]
|
|
##
|
|
## [b]Warning:[/b] It currently does not work offline!
|
|
##
|
|
## @args query
|
|
## @arg-types FirestoreQuery
|
|
## @return Variant representing the array results of the aggregation query
|
|
func aggregation_query(query : FirestoreQuery) -> Variant:
|
|
if query.aggregations.size() == 0:
|
|
Firebase._printerr("Aggregation query sent with no aggregation values: " + str(query))
|
|
return 0
|
|
|
|
var task : FirestoreTask = FirestoreTask.new()
|
|
task.action = FirestoreTask.Task.TASK_AGG_QUERY
|
|
|
|
var body: Dictionary = { structuredAggregationQuery = { structuredQuery = query.query, aggregations = query.aggregations } }
|
|
var url: String = _base_url + _extended_url + _agg_query_suffix
|
|
|
|
task.data = query
|
|
task._fields = JSON.stringify(body)
|
|
task._url = url
|
|
_pooled_request(task)
|
|
var result = await _handle_task_finished(task)
|
|
return result
|
|
|
|
## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return an Array[FirestoreDocument]
|
|
## @args collection_id, page_size, page_token, order_by
|
|
## @arg-types String, int, String, String
|
|
## @arg-defaults , 0, "", ""
|
|
## @return Array[FirestoreDocument]
|
|
func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> Array:
|
|
var task : FirestoreTask = FirestoreTask.new()
|
|
task.action = FirestoreTask.Task.TASK_LIST
|
|
var url : String = _base_url + _extended_url + path
|
|
if page_size != 0:
|
|
url+="?pageSize="+str(page_size)
|
|
if page_token != "":
|
|
url+="&pageToken="+page_token
|
|
if order_by != "":
|
|
url+="&orderBy="+order_by
|
|
|
|
task.data = [path, page_size, page_token, order_by]
|
|
task._url = url
|
|
_pooled_request(task)
|
|
|
|
return await _handle_task_finished(task)
|
|
|
|
|
|
func _set_config(config_json : Dictionary) -> void:
|
|
_config = config_json
|
|
_cache_loc = _config["cacheLocation"]
|
|
_extended_url = _extended_url.replace("[PROJECT_ID]", _config.projectId)
|
|
|
|
# Since caching is causing a lot of issues, I'm removing this check for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted.
|
|
|
|
_check_emulating()
|
|
|
|
func _check_emulating() -> void :
|
|
## Check emulating
|
|
if not Firebase.emulating:
|
|
_base_url = "https://firestore.googleapis.com/{version}/".format({ version = _API_VERSION })
|
|
else:
|
|
var port : String = _config.emulators.ports.firestore
|
|
if port == "":
|
|
Firebase._printerr("You are in 'emulated' mode, but the port for Firestore has not been configured.")
|
|
else:
|
|
_base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port })
|
|
|
|
func _pooled_request(task : FirestoreTask) -> void:
|
|
if (auth == null or auth.is_empty()) and not Firebase.emulating:
|
|
Firebase._print("Unauthenticated request issued...")
|
|
Firebase.Auth.login_anonymous()
|
|
var result : Array = await Firebase.Auth.auth_request
|
|
if result[0] != 1:
|
|
_check_auth_error(result[0], result[1])
|
|
Firebase._print("Client connected as Anonymous")
|
|
|
|
if not Firebase.emulating:
|
|
task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken])
|
|
|
|
var http_request = HTTPRequest.new()
|
|
http_request.timeout = 5
|
|
Utilities.fix_http_request(http_request)
|
|
add_child(http_request)
|
|
http_request.request_completed.connect(
|
|
func(result, response_code, headers, body):
|
|
task._on_request_completed(result, response_code, headers, body)
|
|
http_request.queue_free()
|
|
)
|
|
|
|
http_request.request(task._url, task._headers, task._method, task._fields)
|
|
|
|
func _on_FirebaseAuth_login_succeeded(auth_result : Dictionary) -> void:
|
|
auth = auth_result
|
|
for coll in get_children():
|
|
if coll is FirestoreCollection:
|
|
coll.auth = auth
|
|
|
|
func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
|
|
auth = auth_result
|
|
for coll in get_children():
|
|
if coll is FirestoreCollection:
|
|
coll.auth = auth
|
|
|
|
func _on_FirebaseAuth_logout() -> void:
|
|
auth = {}
|
|
|
|
func _check_auth_error(code : int, message : String) -> void:
|
|
var err : String
|
|
match code:
|
|
400: err = "Please enable the Anonymous Sign-in method, or Authenticate the Client before issuing a request"
|
|
Firebase._printerr(err)
|
|
Firebase._printerr(message)
|
|
|
|
func _handle_task_finished(task : FirestoreTask):
|
|
await task.task_finished
|
|
|
|
if task.error.keys().size() > 0:
|
|
error.emit(task.error)
|
|
|
|
return task.data
|