added firebase and rudimentary leaderboard support
This commit is contained in:
22
addons/godot-firebase/firestore/field_transform.gd
Normal file
22
addons/godot-firebase/firestore/field_transform.gd
Normal file
@@ -0,0 +1,22 @@
|
||||
extends FirestoreTransform
|
||||
class_name FieldTransform
|
||||
|
||||
enum TransformType { SetToServerValue, Maximum, Minimum, Increment, AppendMissingElements, RemoveAllFromArray }
|
||||
|
||||
const transtype_string_map = {
|
||||
TransformType.SetToServerValue : "setToServerValue",
|
||||
TransformType.Increment : "increment",
|
||||
TransformType.Maximum : "maximum",
|
||||
TransformType.Minimum : "minimum",
|
||||
TransformType.AppendMissingElements : "appendMissingElements",
|
||||
TransformType.RemoveAllFromArray : "removeAllFromArray"
|
||||
}
|
||||
|
||||
var document_exists : bool
|
||||
var document_name : String
|
||||
var field_path : String
|
||||
var transform_type : TransformType
|
||||
var value : Variant
|
||||
|
||||
func get_transform_type() -> String:
|
||||
return transtype_string_map[transform_type]
|
||||
1
addons/godot-firebase/firestore/field_transform.gd.uid
Normal file
1
addons/godot-firebase/firestore/field_transform.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dw7p8101sxern
|
||||
35
addons/godot-firebase/firestore/field_transform_array.gd
Normal file
35
addons/godot-firebase/firestore/field_transform_array.gd
Normal file
@@ -0,0 +1,35 @@
|
||||
class_name FieldTransformArray
|
||||
extends RefCounted
|
||||
|
||||
var transforms = []
|
||||
|
||||
var _extended_url
|
||||
var _collection_name
|
||||
const _separator = "/"
|
||||
|
||||
func set_config(config : Dictionary):
|
||||
_extended_url = config.extended_url
|
||||
_collection_name = config.collection_name
|
||||
|
||||
func push_back(transform : FieldTransform) -> void:
|
||||
transforms.push_back(transform)
|
||||
|
||||
func serialize() -> Dictionary:
|
||||
var body = {}
|
||||
var writes_array = []
|
||||
for transform in transforms:
|
||||
writes_array.push_back({
|
||||
"currentDocument": { "exists" : transform.document_exists },
|
||||
"transform" : {
|
||||
"document": _extended_url + _collection_name + _separator + transform.document_name,
|
||||
"fieldTransforms": [
|
||||
{
|
||||
"fieldPath": transform.field_path,
|
||||
transform.get_transform_type(): transform.value
|
||||
}]
|
||||
}
|
||||
})
|
||||
|
||||
body = { "writes": writes_array }
|
||||
|
||||
return body
|
||||
@@ -0,0 +1 @@
|
||||
uid://bopmc0mi55dvu
|
||||
@@ -0,0 +1,19 @@
|
||||
class_name DecrementTransform
|
||||
extends FieldTransform
|
||||
|
||||
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void:
|
||||
document_name = doc_name
|
||||
document_exists = doc_must_exist
|
||||
field_path = path_to_field
|
||||
|
||||
transform_type = FieldTransform.TransformType.Increment
|
||||
|
||||
var value_type = typeof(by_this_much)
|
||||
if value_type == TYPE_INT:
|
||||
self.value = {
|
||||
"integerValue": -by_this_much
|
||||
}
|
||||
elif value_type == TYPE_FLOAT:
|
||||
self.value = {
|
||||
"doubleValue": -by_this_much
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bit26sxq4daw7
|
||||
@@ -0,0 +1,19 @@
|
||||
class_name IncrementTransform
|
||||
extends FieldTransform
|
||||
|
||||
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, by_this_much : Variant) -> void:
|
||||
document_name = doc_name
|
||||
document_exists = doc_must_exist
|
||||
field_path = path_to_field
|
||||
|
||||
transform_type = FieldTransform.TransformType.Increment
|
||||
|
||||
var value_type = typeof(by_this_much)
|
||||
if value_type == TYPE_INT:
|
||||
self.value = {
|
||||
"integerValue": by_this_much
|
||||
}
|
||||
elif value_type == TYPE_FLOAT:
|
||||
self.value = {
|
||||
"doubleValue": by_this_much
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c5gx3a3dsmwop
|
||||
@@ -0,0 +1,19 @@
|
||||
class_name MaxTransform
|
||||
extends FieldTransform
|
||||
|
||||
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void:
|
||||
document_name = doc_name
|
||||
document_exists = doc_must_exist
|
||||
field_path = path_to_field
|
||||
|
||||
transform_type = FieldTransform.TransformType.Maximum
|
||||
|
||||
var value_type = typeof(value)
|
||||
if value_type == TYPE_INT:
|
||||
self.value = {
|
||||
"integerValue": value
|
||||
}
|
||||
elif value_type == TYPE_FLOAT:
|
||||
self.value = {
|
||||
"doubleValue": value
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://xf5c8b0lrjpl
|
||||
@@ -0,0 +1,19 @@
|
||||
class_name MinTransform
|
||||
extends FieldTransform
|
||||
|
||||
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String, value : Variant) -> void:
|
||||
document_name = doc_name
|
||||
document_exists = doc_must_exist
|
||||
field_path = path_to_field
|
||||
|
||||
transform_type = FieldTransform.TransformType.Minimum
|
||||
|
||||
var value_type = typeof(value)
|
||||
if value_type == TYPE_INT:
|
||||
self.value = {
|
||||
"integerValue": value
|
||||
}
|
||||
elif value_type == TYPE_FLOAT:
|
||||
self.value = {
|
||||
"doubleValue": value
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://cei7mxm5uqrkc
|
||||
@@ -0,0 +1,10 @@
|
||||
class_name ServerTimestampTransform
|
||||
extends FieldTransform
|
||||
|
||||
func _init(doc_name : String, doc_must_exist : bool, path_to_field : String) -> void:
|
||||
document_name = doc_name
|
||||
document_exists = doc_must_exist
|
||||
field_path = path_to_field
|
||||
|
||||
transform_type = FieldTransform.TransformType.SetToServerValue
|
||||
value = "REQUEST_TIME"
|
||||
@@ -0,0 +1 @@
|
||||
uid://cqkqdex0s16id
|
||||
243
addons/godot-firebase/firestore/firestore.gd
Normal file
243
addons/godot-firebase/firestore/firestore.gd
Normal file
@@ -0,0 +1,243 @@
|
||||
## @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
|
||||
1
addons/godot-firebase/firestore/firestore.gd.uid
Normal file
1
addons/godot-firebase/firestore/firestore.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c2n2dkjpnwcsd
|
||||
178
addons/godot-firebase/firestore/firestore_collection.gd
Normal file
178
addons/godot-firebase/firestore/firestore_collection.gd
Normal file
@@ -0,0 +1,178 @@
|
||||
## @meta-authors TODO
|
||||
## @meta-authors TODO
|
||||
## @meta-version 2.3
|
||||
## A reference to a Firestore Collection.
|
||||
## Documentation TODO.
|
||||
@tool
|
||||
class_name FirestoreCollection
|
||||
extends Node
|
||||
|
||||
signal error(error_result)
|
||||
|
||||
const _AUTHORIZATION_HEADER : String = "Authorization: Bearer "
|
||||
|
||||
const _separator : String = "/"
|
||||
const _query_tag : String = "?"
|
||||
const _documentId_tag : String = "documentId="
|
||||
|
||||
var auth : Dictionary
|
||||
var collection_name : String
|
||||
|
||||
var _base_url : String
|
||||
var _extended_url : String
|
||||
var _config : Dictionary
|
||||
|
||||
var _documents := {}
|
||||
|
||||
# ----------------------- Requests
|
||||
|
||||
## @args document_id
|
||||
## @return FirestoreTask
|
||||
## used to GET a document from the collection, specify @document_id
|
||||
func get_doc(document_id : String, from_cache : bool = false, is_listener : bool = false) -> FirestoreDocument:
|
||||
if from_cache:
|
||||
# for now, just return the child directly; in the future, make it smarter so there's a default, if long, polling time for this
|
||||
for child in get_children():
|
||||
if child.doc_name == document_id:
|
||||
return child
|
||||
|
||||
var task : FirestoreTask = FirestoreTask.new()
|
||||
task.action = FirestoreTask.Task.TASK_GET
|
||||
task.data = collection_name + "/" + document_id
|
||||
var url = _get_request_url() + _separator + document_id.replace(" ", "%20")
|
||||
|
||||
_process_request(task, document_id, url)
|
||||
var result = await Firebase.Firestore._handle_task_finished(task)
|
||||
if result != null:
|
||||
for child in get_children():
|
||||
if child.doc_name == document_id:
|
||||
child.replace(result, true)
|
||||
result = child
|
||||
break
|
||||
else:
|
||||
print("get_document returned null for %s %s" % [collection_name, document_id])
|
||||
|
||||
return result
|
||||
|
||||
## @args document_id, fields
|
||||
## @arg-defaults , {}
|
||||
## @return FirestoreDocument
|
||||
## used to ADD a new document to the collection, specify @documentID and @data
|
||||
func add(document_id : String, data : Dictionary = {}) -> FirestoreDocument:
|
||||
var task : FirestoreTask = FirestoreTask.new()
|
||||
task.action = FirestoreTask.Task.TASK_POST
|
||||
task.data = collection_name + "/" + document_id
|
||||
var url = _get_request_url() + _query_tag + _documentId_tag + document_id
|
||||
|
||||
_process_request(task, document_id, url, JSON.stringify(Utilities.dict2fields(data)))
|
||||
var result = await Firebase.Firestore._handle_task_finished(task)
|
||||
if result != null:
|
||||
for child in get_children():
|
||||
if child.doc_name == document_id:
|
||||
child.free() # Consider throwing an error for this since it shouldn't already exist
|
||||
break
|
||||
|
||||
result.collection_name = collection_name
|
||||
add_child(result, true)
|
||||
return result
|
||||
|
||||
## @args document
|
||||
## @return FirestoreDocument
|
||||
# used to UPDATE a document, specify the document
|
||||
func update(document : FirestoreDocument) -> FirestoreDocument:
|
||||
var task : FirestoreTask = FirestoreTask.new()
|
||||
task.action = FirestoreTask.Task.TASK_PATCH
|
||||
task.data = collection_name + "/" + document.doc_name
|
||||
var url = _get_request_url() + _separator + document.doc_name.replace(" ", "%20") + "?"
|
||||
for key in document.keys():
|
||||
url+="updateMask.fieldPaths={key}&".format({key = key})
|
||||
|
||||
url = url.rstrip("&")
|
||||
|
||||
for key in document.keys():
|
||||
if document.get_value(key) == null:
|
||||
document._erase(key)
|
||||
|
||||
var temp_transforms
|
||||
if document._transforms != null:
|
||||
temp_transforms = document._transforms
|
||||
document._transforms = null
|
||||
|
||||
var body = JSON.stringify({"fields": document.document})
|
||||
|
||||
_process_request(task, document.doc_name, url, body)
|
||||
var result = await Firebase.Firestore._handle_task_finished(task)
|
||||
if result != null:
|
||||
for child in get_children():
|
||||
if child.doc_name == result.doc_name:
|
||||
child.replace(result, true)
|
||||
break
|
||||
|
||||
if temp_transforms != null:
|
||||
result._transforms = temp_transforms
|
||||
|
||||
return result
|
||||
|
||||
|
||||
## @args document
|
||||
## @return Dictionary
|
||||
# Used to commit changes from transforms, specify the document with the transforms
|
||||
func commit(document : FirestoreDocument) -> Dictionary:
|
||||
var task : FirestoreTask = FirestoreTask.new()
|
||||
task.action = FirestoreTask.Task.TASK_COMMIT
|
||||
var url = get_database_url("commit")
|
||||
|
||||
document._transforms.set_config(
|
||||
{
|
||||
"extended_url": _extended_url,
|
||||
"collection_name": collection_name
|
||||
}
|
||||
) # Only place we can set this is here, oofness
|
||||
|
||||
var body = document._transforms.serialize()
|
||||
document.clear_field_transforms()
|
||||
_process_request(task, document.doc_name, url, JSON.stringify(body))
|
||||
|
||||
return await Firebase.Firestore._handle_task_finished(task) # Not implementing the follow-up get here as user may have a listener that's already listening for changes, but user should call get if they don't
|
||||
|
||||
## @args document_id
|
||||
## @return FirestoreTask
|
||||
# used to DELETE a document, specify the document
|
||||
func delete(document : FirestoreDocument) -> bool:
|
||||
var doc_name = document.doc_name
|
||||
var task : FirestoreTask = FirestoreTask.new()
|
||||
task.action = FirestoreTask.Task.TASK_DELETE
|
||||
task.data = document.collection_name + "/" + doc_name
|
||||
var url = _get_request_url() + _separator + doc_name.replace(" ", "%20")
|
||||
_process_request(task, doc_name, url)
|
||||
var result = await Firebase.Firestore._handle_task_finished(task)
|
||||
|
||||
# Clean up the cache
|
||||
if result:
|
||||
for node in get_children():
|
||||
if node.doc_name == doc_name:
|
||||
node.free() # Should be only one
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
func _get_request_url() -> String:
|
||||
return _base_url + _extended_url + collection_name
|
||||
|
||||
func _process_request(task : FirestoreTask, document_id : String, url : String, fields := "") -> void:
|
||||
if auth == null or auth.is_empty():
|
||||
Firebase._print("Unauthenticated request issued...")
|
||||
Firebase.Auth.login_anonymous()
|
||||
var result : Array = await Firebase.Auth.auth_request
|
||||
if result[0] != 1:
|
||||
Firebase.Firestore._check_auth_error(result[0], result[1])
|
||||
return
|
||||
Firebase._print("Client authenticated as Anonymous User.")
|
||||
|
||||
task._url = url
|
||||
task._fields = fields
|
||||
task._headers = PackedStringArray([_AUTHORIZATION_HEADER + auth.idtoken])
|
||||
Firebase.Firestore._pooled_request(task)
|
||||
|
||||
func get_database_url(append) -> String:
|
||||
return _base_url + _extended_url.rstrip("/") + ":" + append
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnvqqpj5cima0
|
||||
185
addons/godot-firebase/firestore/firestore_document.gd
Normal file
185
addons/godot-firebase/firestore/firestore_document.gd
Normal file
@@ -0,0 +1,185 @@
|
||||
## @meta-authors Kyle Szklenski
|
||||
## @meta-version 2.2
|
||||
## A reference to a Firestore Document.
|
||||
## Documentation TODO.
|
||||
@tool
|
||||
class_name FirestoreDocument
|
||||
extends Node
|
||||
|
||||
# A FirestoreDocument objects that holds all important values for a Firestore Document,
|
||||
# @doc_name = name of the Firestore Document, which is the request PATH
|
||||
# @doc_fields = fields held by Firestore Document, in APIs format
|
||||
# created when requested from a `collection().get()` call
|
||||
|
||||
var document : Dictionary # the Document itself
|
||||
var doc_name : String # only .name
|
||||
var create_time : String # createTime
|
||||
var collection_name : String # Name of the collection to which it belongs
|
||||
var _transforms : FieldTransformArray # The transforms to apply
|
||||
signal changed(changes)
|
||||
|
||||
func _init(doc : Dictionary = {}):
|
||||
_transforms = FieldTransformArray.new()
|
||||
|
||||
if doc.has("fields"):
|
||||
document = doc.fields
|
||||
if doc.has("name"):
|
||||
doc_name = doc.name
|
||||
if doc_name.count("/") > 2:
|
||||
doc_name = (doc_name.split("/") as Array).back()
|
||||
if doc.has("createTime"):
|
||||
self.create_time = doc.createTime
|
||||
|
||||
func replace(with : FirestoreDocument, is_listener := false) -> void:
|
||||
var current = document.duplicate()
|
||||
document = with.document
|
||||
|
||||
var changes = {
|
||||
"added": [], "removed": [], "updated": [], "is_listener": is_listener
|
||||
}
|
||||
|
||||
for key in current.keys():
|
||||
if not document.has(key):
|
||||
changes.removed.push_back({ "key" : key })
|
||||
else:
|
||||
var new_value = Utilities.from_firebase_type(document[key])
|
||||
var old_value = Utilities.from_firebase_type(current[key])
|
||||
if typeof(new_value) != typeof(old_value) or new_value != old_value:
|
||||
if old_value == null:
|
||||
changes.removed.push_back({ "key" : key }) # ??
|
||||
else:
|
||||
changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value })
|
||||
|
||||
for key in document.keys():
|
||||
if not current.has(key):
|
||||
changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) })
|
||||
|
||||
if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()):
|
||||
changed.emit(changes)
|
||||
|
||||
func new_document(base_document: Dictionary) -> void:
|
||||
var current = document.duplicate()
|
||||
document = {}
|
||||
for key in base_document.keys():
|
||||
document[key] = Utilities.to_firebase_type(key)
|
||||
|
||||
var changes = {
|
||||
"added": [], "removed": [], "updated": [], "is_listener": false
|
||||
}
|
||||
|
||||
for key in current.keys():
|
||||
if not document.has(key):
|
||||
changes.removed.push_back({ "key" : key })
|
||||
else:
|
||||
var new_value = Utilities.from_firebase_type(document[key])
|
||||
var old_value = Utilities.from_firebase_type(current[key])
|
||||
if typeof(new_value) != typeof(old_value) or new_value != old_value:
|
||||
if old_value == null:
|
||||
changes.removed.push_back({ "key" : key }) # ??
|
||||
else:
|
||||
changes.updated.push_back({ "key" : key, "old": old_value, "new" : new_value })
|
||||
|
||||
for key in document.keys():
|
||||
if not current.has(key):
|
||||
changes.added.push_back({ "key" : key, "new" : Utilities.from_firebase_type(document[key]) })
|
||||
|
||||
if not (changes.added.is_empty() and changes.removed.is_empty() and changes.updated.is_empty()):
|
||||
changed.emit(changes)
|
||||
|
||||
func is_null_value(key) -> bool:
|
||||
return document.has(key) and Utilities.from_firebase_type(document[key]) == null
|
||||
|
||||
# As of right now, we do not track these with track changes; instead, they'll come back when the document updates from the server.
|
||||
# Until that time, it's expected if you want to track these types of changes that you commit for the transforms and then get the document yourself.
|
||||
func add_field_transform(transform : FieldTransform) -> void:
|
||||
_transforms.push_back(transform)
|
||||
|
||||
func remove_field_transform(transform : FieldTransform) -> void:
|
||||
_transforms.erase(transform)
|
||||
|
||||
func clear_field_transforms() -> void:
|
||||
_transforms.transforms.clear()
|
||||
|
||||
func remove_field(field_path : String) -> void:
|
||||
if document.has(field_path):
|
||||
document[field_path] = Utilities.to_firebase_type(null)
|
||||
|
||||
var changes = {
|
||||
"added": [], "removed": [], "updated": [], "is_listener": false
|
||||
}
|
||||
|
||||
changes.removed.push_back({ "key" : field_path })
|
||||
changed.emit(changes)
|
||||
|
||||
func _erase(field_path : String) -> void:
|
||||
document.erase(field_path)
|
||||
|
||||
func add_or_update_field(field_path : String, value : Variant) -> void:
|
||||
var changes = {
|
||||
"added": [], "removed": [], "updated": [], "is_listener": false
|
||||
}
|
||||
|
||||
var existing_value = get_value(field_path)
|
||||
var has_field_path = existing_value != null and not is_null_value(field_path)
|
||||
|
||||
var converted_value = Utilities.to_firebase_type(value)
|
||||
document[field_path] = converted_value
|
||||
|
||||
if has_field_path:
|
||||
changes.updated.push_back({ "key" : field_path, "old" : existing_value, "new" : value })
|
||||
else:
|
||||
changes.added.push_back({ "key" : field_path, "new" : value })
|
||||
|
||||
changed.emit(changes)
|
||||
|
||||
func on_snapshot(when_called : Callable, poll_time : float = 1.0) -> FirestoreListener.FirestoreListenerConnection:
|
||||
if get_child_count() >= 1: # Only one listener per
|
||||
assert(false, "Multiple listeners not allowed for the same document yet")
|
||||
return
|
||||
|
||||
changed.connect(when_called, CONNECT_REFERENCE_COUNTED)
|
||||
var listener = preload("res://addons/godot-firebase/firestore/firestore_listener.tscn").instantiate()
|
||||
add_child(listener)
|
||||
listener.initialize_listener(collection_name, doc_name, poll_time)
|
||||
listener.owner = self
|
||||
var result = listener.enable_connection()
|
||||
return result
|
||||
|
||||
func get_value(property : StringName) -> Variant:
|
||||
if property == "doc_name":
|
||||
return doc_name
|
||||
elif property == "collection_name":
|
||||
return collection_name
|
||||
elif property == "create_time":
|
||||
return create_time
|
||||
|
||||
if document.has(property):
|
||||
var result = Utilities.from_firebase_type(document[property])
|
||||
return result
|
||||
|
||||
return null
|
||||
|
||||
func _get(property: StringName) -> Variant:
|
||||
return get_value(property)
|
||||
|
||||
func _set(property: StringName, value: Variant) -> bool:
|
||||
assert(value != null, "When using the dictionary setter, the value cannot be null; use erase_field instead.")
|
||||
document[property] = Utilities.to_firebase_type(value)
|
||||
return true
|
||||
|
||||
func get_unsafe_document() -> Dictionary:
|
||||
var result = {}
|
||||
for key in keys():
|
||||
result[key] = Utilities.from_firebase_type(document[key])
|
||||
|
||||
return result
|
||||
|
||||
func keys():
|
||||
return document.keys()
|
||||
|
||||
# Call print(document) to return directly this document formatted
|
||||
func _to_string() -> String:
|
||||
return ("doc_name: {doc_name}, \ndata: {data}, \ncreate_time: {create_time}\n").format(
|
||||
{doc_name = self.doc_name,
|
||||
data = document,
|
||||
create_time = self.create_time})
|
||||
@@ -0,0 +1 @@
|
||||
uid://lvx6e1rnbjha
|
||||
47
addons/godot-firebase/firestore/firestore_listener.gd
Normal file
47
addons/godot-firebase/firestore/firestore_listener.gd
Normal file
@@ -0,0 +1,47 @@
|
||||
class_name FirestoreListener
|
||||
extends Node
|
||||
|
||||
const MinPollTime = 60 * 2 # seconds, so 2 minutes
|
||||
|
||||
var _doc_name : String
|
||||
var _poll_time : float
|
||||
var _collection : FirestoreCollection
|
||||
|
||||
var _total_time = 0.0
|
||||
var _enabled := false
|
||||
|
||||
func initialize_listener(collection_name : String, doc_name : String, poll_time : float) -> void:
|
||||
_poll_time = max(poll_time, MinPollTime)
|
||||
_doc_name = doc_name
|
||||
_collection = Firebase.Firestore.collection(collection_name)
|
||||
|
||||
func enable_connection() -> FirestoreListenerConnection:
|
||||
_enabled = true
|
||||
set_process(true)
|
||||
return FirestoreListenerConnection.new(self)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _enabled:
|
||||
_total_time += delta
|
||||
if _total_time >= _poll_time:
|
||||
_check_for_server_updates()
|
||||
_total_time = 0.0
|
||||
|
||||
func _check_for_server_updates() -> void:
|
||||
var executor = func():
|
||||
var doc = await _collection.get_doc(_doc_name, false, true)
|
||||
if doc == null:
|
||||
set_process(false) # Document was deleted out from under us, so stop updating
|
||||
|
||||
executor.call() # Hack to work around the await here, otherwise would have to call with await in _process and that's no bueno
|
||||
|
||||
class FirestoreListenerConnection extends RefCounted:
|
||||
var connection
|
||||
|
||||
func _init(connection_node):
|
||||
connection = connection_node
|
||||
|
||||
func stop():
|
||||
if connection != null and is_instance_valid(connection):
|
||||
connection.set_process(false)
|
||||
connection.free()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnn8dx3q452pr
|
||||
6
addons/godot-firebase/firestore/firestore_listener.tscn
Normal file
6
addons/godot-firebase/firestore/firestore_listener.tscn
Normal file
@@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bwv7vtgssc0n5"]
|
||||
|
||||
[ext_resource type="Script" path="res://addons/godot-firebase/firestore/firestore_listener.gd" id="1_qlaei"]
|
||||
|
||||
[node name="FirestoreListener" type="Node"]
|
||||
script = ExtResource("1_qlaei")
|
||||
255
addons/godot-firebase/firestore/firestore_query.gd
Normal file
255
addons/godot-firebase/firestore/firestore_query.gd
Normal file
@@ -0,0 +1,255 @@
|
||||
## @meta-authors Nicoló 'fenix' Santilio, Kyle Szklenski
|
||||
## @meta-version 1.4
|
||||
## A firestore query.
|
||||
## Documentation TODO.
|
||||
@tool
|
||||
extends RefCounted
|
||||
class_name FirestoreQuery
|
||||
|
||||
class Order:
|
||||
var obj: Dictionary
|
||||
|
||||
class Cursor:
|
||||
var values: Array
|
||||
var before: bool
|
||||
|
||||
func _init(v : Array,b : bool):
|
||||
values = v
|
||||
before = b
|
||||
|
||||
signal query_result(query_result)
|
||||
|
||||
const TEMPLATE_QUERY: Dictionary = {
|
||||
select = {},
|
||||
from = [],
|
||||
where = {},
|
||||
orderBy = [],
|
||||
startAt = {},
|
||||
endAt = {},
|
||||
offset = 0,
|
||||
limit = 0
|
||||
}
|
||||
|
||||
var query: Dictionary = {}
|
||||
var aggregations: Array[Dictionary] = []
|
||||
var sub_collection_path: String = ""
|
||||
|
||||
enum OPERATOR {
|
||||
# Standard operators
|
||||
OPERATOR_UNSPECIFIED,
|
||||
LESS_THAN,
|
||||
LESS_THAN_OR_EQUAL,
|
||||
GREATER_THAN,
|
||||
GREATER_THAN_OR_EQUAL,
|
||||
EQUAL,
|
||||
NOT_EQUAL,
|
||||
ARRAY_CONTAINS,
|
||||
ARRAY_CONTAINS_ANY,
|
||||
IN,
|
||||
NOT_IN,
|
||||
|
||||
# Unary operators
|
||||
IS_NAN,
|
||||
IS_NULL,
|
||||
IS_NOT_NAN,
|
||||
IS_NOT_NULL,
|
||||
|
||||
# Complex operators
|
||||
AND,
|
||||
OR
|
||||
}
|
||||
|
||||
enum DIRECTION {
|
||||
DIRECTION_UNSPECIFIED,
|
||||
ASCENDING,
|
||||
DESCENDING
|
||||
}
|
||||
|
||||
|
||||
# Select which fields you want to return as a reflection from your query.
|
||||
# Fields must be added inside a list. Only a field is accepted inside the list
|
||||
# Leave the Array empty if you want to return the whole document
|
||||
func select(fields) -> FirestoreQuery:
|
||||
match typeof(fields):
|
||||
TYPE_STRING:
|
||||
query["select"] = { fields = { fieldPath = fields } }
|
||||
TYPE_ARRAY:
|
||||
for field in fields:
|
||||
field = ({ fieldPath = field })
|
||||
query["select"] = { fields = fields }
|
||||
_:
|
||||
print("Type of 'fields' is not accepted.")
|
||||
return self
|
||||
|
||||
|
||||
|
||||
# Select the collection you want to return the query result from
|
||||
# if @all_descendants also sub-collections will be returned. If false, only documents will be returned
|
||||
func from(collection_id : String, all_descendants : bool = true) -> FirestoreQuery:
|
||||
query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}]
|
||||
return self
|
||||
|
||||
# @collections_array MUST be an Array of Arrays with this structure
|
||||
# [ ["collection_id", true/false] ]
|
||||
func from_many(collections_array : Array) -> FirestoreQuery:
|
||||
var collections : Array = []
|
||||
for collection in collections_array:
|
||||
collections.append({collectionId = collection[0], allDescendants = collection[1]})
|
||||
query["from"] = collections.duplicate(true)
|
||||
return self
|
||||
|
||||
|
||||
# Query the value of a field you want to match
|
||||
# @field : the name of the field
|
||||
# @operator : from FirestoreQuery.OPERATOR
|
||||
# @value : can be any type - String, int, bool, float
|
||||
# @chain : from FirestoreQuery.OPERATOR.[OR/AND], use it only if you want to chain "AND" or "OR" logic with futher where() calls
|
||||
# eg. super.where("name", OPERATOR.EQUAL, "Matt", OPERATOR.AND).where("age", OPERATOR.LESS_THAN, 20)
|
||||
func where(field : String, operator : int, value = null, chain : int = -1):
|
||||
if operator in [OPERATOR.IS_NAN, OPERATOR.IS_NULL, OPERATOR.IS_NOT_NAN, OPERATOR.IS_NOT_NULL]:
|
||||
if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")):
|
||||
var filters : Array = []
|
||||
if query.has("where") and query.where.has("compositeFilter"):
|
||||
if chain == -1:
|
||||
filters = query.where.compositeFilter.filters.duplicate(true)
|
||||
chain = OPERATOR.get(query.where.compositeFilter.op)
|
||||
else:
|
||||
filters.append(query.where)
|
||||
filters.append(create_unary_filter(field, operator))
|
||||
query["where"] = create_composite_filter(chain, filters)
|
||||
else:
|
||||
query["where"] = create_unary_filter(field, operator)
|
||||
else:
|
||||
if value == null:
|
||||
print("A value must be defined to match the field: {field}".format({field = field}))
|
||||
else:
|
||||
if (chain in [OPERATOR.AND, OPERATOR.OR]) or (query.has("where") and query.where.has("compositeFilter")):
|
||||
var filters : Array = []
|
||||
if query.has("where") and query.where.has("compositeFilter"):
|
||||
if chain == -1:
|
||||
filters = query.where.compositeFilter.filters.duplicate(true)
|
||||
chain = OPERATOR.get(query.where.compositeFilter.op)
|
||||
else:
|
||||
filters.append(query.where)
|
||||
filters.append(create_field_filter(field, operator, value))
|
||||
query["where"] = create_composite_filter(chain, filters)
|
||||
else:
|
||||
query["where"] = create_field_filter(field, operator, value)
|
||||
return self
|
||||
|
||||
|
||||
# Order by a field, defining its name and the order direction
|
||||
# default directoin = Ascending
|
||||
func order_by(field : String, direction : int = DIRECTION.ASCENDING) -> FirestoreQuery:
|
||||
query["orderBy"] = [_order_object(field, direction).obj]
|
||||
return self
|
||||
|
||||
|
||||
# Order by a set of fields and directions
|
||||
# @order_list is an Array of Arrays with the following structure
|
||||
# [@field_name , @DIRECTION.[direction]]
|
||||
# else, order_object() can be called to return an already parsed Dictionary
|
||||
func order_by_fields(order_field_list : Array) -> FirestoreQuery:
|
||||
var order_list : Array = []
|
||||
for order in order_field_list:
|
||||
if order is Array:
|
||||
order_list.append(_order_object(order[0], order[1]).obj)
|
||||
elif order is Order:
|
||||
order_list.append(order.obj)
|
||||
query["orderBy"] = order_list
|
||||
return self
|
||||
|
||||
func start_at(value, before : bool) -> FirestoreQuery:
|
||||
var cursor : Cursor = _cursor_object(value, before)
|
||||
query["startAt"] = { values = cursor.values, before = cursor.before }
|
||||
print(query["startAt"])
|
||||
return self
|
||||
|
||||
|
||||
func end_at(value, before : bool) -> FirestoreQuery:
|
||||
var cursor : Cursor = _cursor_object(value, before)
|
||||
query["startAt"] = { values = cursor.values, before = cursor.before }
|
||||
print(query["startAt"])
|
||||
return self
|
||||
|
||||
|
||||
func offset(offset : int) -> FirestoreQuery:
|
||||
if offset < 0:
|
||||
print("If specified, offset must be >= 0")
|
||||
else:
|
||||
query["offset"] = offset
|
||||
return self
|
||||
|
||||
|
||||
func limit(limit : int) -> FirestoreQuery:
|
||||
if limit < 0:
|
||||
print("If specified, offset must be >= 0")
|
||||
else:
|
||||
query["limit"] = limit
|
||||
return self
|
||||
|
||||
|
||||
func aggregate() -> FirestoreAggregation:
|
||||
return FirestoreAggregation.new(self)
|
||||
|
||||
class FirestoreAggregation extends RefCounted:
|
||||
var _query: FirestoreQuery
|
||||
|
||||
func _init(query: FirestoreQuery) -> void:
|
||||
_query = query
|
||||
|
||||
func sum(field: String) -> FirestoreQuery:
|
||||
_query.aggregations.push_back({ sum = { field = { fieldPath = field }}})
|
||||
return _query
|
||||
|
||||
func count(up_to: int) -> FirestoreQuery:
|
||||
_query.aggregations.push_back({ count = { upTo = up_to }})
|
||||
return _query
|
||||
|
||||
func average(field: String) -> FirestoreQuery:
|
||||
_query.aggregations.push_back({ avg = { field = { fieldPath = field }}})
|
||||
return _query
|
||||
|
||||
# UTILITIES ----------------------------------------
|
||||
|
||||
static func _cursor_object(value, before : bool) -> Cursor:
|
||||
var parse : Dictionary = Utilities.dict2fields({value = value}).fields.value
|
||||
var cursor : Cursor = Cursor.new(parse.arrayValue.values if parse.has("arrayValue") else [parse], before)
|
||||
return cursor
|
||||
|
||||
static func _order_object(field : String, direction : int) -> Order:
|
||||
var order : Order = Order.new()
|
||||
order.obj = { field = { fieldPath = field }, direction = DIRECTION.keys()[direction] }
|
||||
return order
|
||||
|
||||
|
||||
func create_field_filter(field : String, operator : int, value) -> Dictionary:
|
||||
return {
|
||||
fieldFilter = {
|
||||
field = { fieldPath = field },
|
||||
op = OPERATOR.keys()[operator],
|
||||
value = Utilities.dict2fields({value = value}).fields.value
|
||||
} }
|
||||
|
||||
func create_unary_filter(field : String, operator : int) -> Dictionary:
|
||||
return {
|
||||
unaryFilter = {
|
||||
field = { fieldPath = field },
|
||||
op = OPERATOR.keys()[operator],
|
||||
} }
|
||||
|
||||
func create_composite_filter(operator : int, filters : Array) -> Dictionary:
|
||||
return {
|
||||
compositeFilter = {
|
||||
op = OPERATOR.keys()[operator],
|
||||
filters = filters
|
||||
} }
|
||||
|
||||
func clean() -> void:
|
||||
query = { }
|
||||
|
||||
func _to_string() -> String:
|
||||
var pretty : String = "QUERY:\n"
|
||||
for key in query.keys():
|
||||
pretty += "- {key} = {value}\n".format({key = key, value = query.get(key)})
|
||||
return pretty
|
||||
1
addons/godot-firebase/firestore/firestore_query.gd.uid
Normal file
1
addons/godot-firebase/firestore/firestore_query.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c7laxjcm52kh5
|
||||
188
addons/godot-firebase/firestore/firestore_task.gd
Normal file
188
addons/godot-firebase/firestore/firestore_task.gd
Normal file
@@ -0,0 +1,188 @@
|
||||
## @meta-authors Nicolò 'fenix' Santilio, Kyle 'backat50ft' Szklenski
|
||||
## @meta-version 1.4
|
||||
##
|
||||
## A [code]FirestoreTask[/code] is an independent node inheriting [code]HTTPRequest[/code] that processes a [code]Firestore[/code] request.
|
||||
## Once the Task is completed (both if successfully or not) it will emit the relative signal (or a general purpose signal [code]task_finished()[/code]) and will destroy automatically.
|
||||
##
|
||||
## Being a [code]Node[/code] it can be stored in a variable to yield checked it, and receive its result as a callback.
|
||||
## All signals emitted by a [code]FirestoreTask[/code] represent a direct level of signal communication, which can be high ([code]get_document(document), result_query(result)[/code]) or low ([code]task_finished(result)[/code]).
|
||||
## An indirect level of communication with Tasks is also provided, redirecting signals to the [class FirebaseFirestore] module.
|
||||
##
|
||||
## ex.
|
||||
## [code]var task : FirestoreTask = Firebase.Firestore.query(query)[/code]
|
||||
## [code]var result : Array = await task.task_finished[/code]
|
||||
## [code]var result : Array = await task.result_query[/code]
|
||||
## [code]var result : Array = await Firebase.Firestore.task_finished[/code]
|
||||
## [code]var result : Array = await Firebase.Firestore.result_query[/code]
|
||||
##
|
||||
## @tutorial https://github.com/GodotNuts/GodotFirebase/wiki/Firestore#FirestoreTask
|
||||
|
||||
@tool
|
||||
class_name FirestoreTask
|
||||
extends RefCounted
|
||||
|
||||
## Emitted when a request is completed. The request can be successful or not successful: if not, an [code]error[/code] Dictionary will be passed as a result.
|
||||
## @arg-types Variant
|
||||
signal task_finished()
|
||||
|
||||
enum Task {
|
||||
TASK_GET, ## A GET Request Task, processing a get() request
|
||||
TASK_POST, ## A POST Request Task, processing add() request
|
||||
TASK_PATCH, ## A PATCH Request Task, processing a update() request
|
||||
TASK_DELETE, ## A DELETE Request Task, processing a delete() request
|
||||
TASK_QUERY, ## A POST Request Task, processing a query() request
|
||||
TASK_AGG_QUERY, ## A POST Request Task, processing an aggregation_query() request
|
||||
TASK_LIST, ## A POST Request Task, processing a list() request
|
||||
TASK_COMMIT ## A POST Request Task that hits the write api
|
||||
}
|
||||
|
||||
## Mapping of Task enum values to descriptions for use in printing user-friendly error codes.
|
||||
const TASK_MAP = {
|
||||
Task.TASK_GET: "GET DOCUMENT",
|
||||
Task.TASK_POST: "ADD DOCUMENT",
|
||||
Task.TASK_PATCH: "UPDATE DOCUMENT",
|
||||
Task.TASK_DELETE: "DELETE DOCUMENT",
|
||||
Task.TASK_QUERY: "QUERY COLLECTION",
|
||||
Task.TASK_LIST: "LIST DOCUMENTS",
|
||||
Task.TASK_COMMIT: "COMMIT DOCUMENT",
|
||||
Task.TASK_AGG_QUERY: "AGG QUERY COLLECTION"
|
||||
}
|
||||
|
||||
## The code indicating the request Firestore is processing.
|
||||
## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers.
|
||||
## @setter set_action
|
||||
var action : int = -1 : set = set_action
|
||||
|
||||
## A variable, temporary holding the result of the request.
|
||||
var data
|
||||
var error: Dictionary
|
||||
var document: FirestoreDocument
|
||||
|
||||
var _response_headers: PackedStringArray = PackedStringArray()
|
||||
var _response_code: int = 0
|
||||
|
||||
var _method: int = -1
|
||||
var _url: String = ""
|
||||
var _fields: String = ""
|
||||
var _headers: PackedStringArray = []
|
||||
|
||||
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
|
||||
var bod = body.get_string_from_utf8()
|
||||
if bod != "":
|
||||
bod = Utilities.get_json_data(bod)
|
||||
|
||||
var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK
|
||||
# Probably going to regret this...
|
||||
if response_code == HTTPClient.RESPONSE_OK:
|
||||
match action:
|
||||
Task.TASK_POST, Task.TASK_GET, Task.TASK_PATCH:
|
||||
document = FirestoreDocument.new(bod)
|
||||
data = document
|
||||
Task.TASK_DELETE:
|
||||
data = true
|
||||
Task.TASK_QUERY:
|
||||
data = []
|
||||
for doc in bod:
|
||||
if doc.has('document'):
|
||||
data.append(FirestoreDocument.new(doc.document))
|
||||
Task.TASK_AGG_QUERY:
|
||||
var agg_results = []
|
||||
for agg_result in bod:
|
||||
var idx = 0
|
||||
var query_results = {}
|
||||
for field_value in agg_result.result.aggregateFields.keys():
|
||||
var agg = data.aggregations[idx]
|
||||
var field = agg_result.result.aggregateFields[field_value]
|
||||
query_results[agg.keys()[0]] = Utilities.from_firebase_type(field)
|
||||
idx += 1
|
||||
agg_results.push_back(query_results)
|
||||
data = agg_results
|
||||
Task.TASK_LIST:
|
||||
data = []
|
||||
if bod.has('documents'):
|
||||
for doc in bod.documents:
|
||||
data.append(FirestoreDocument.new(doc))
|
||||
if bod.has("nextPageToken"):
|
||||
data.append(bod.nextPageToken)
|
||||
Task.TASK_COMMIT:
|
||||
data = bod # Commit's response is not a full document, so don't treat it as such
|
||||
else:
|
||||
var description = ""
|
||||
if TASK_MAP.has(action):
|
||||
description = "(" + TASK_MAP[action] + ")"
|
||||
|
||||
Firebase._printerr("Action in error was: " + str(action) + " " + description)
|
||||
build_error(bod, action, description)
|
||||
|
||||
task_finished.emit()
|
||||
|
||||
func build_error(_error, action, description) -> void:
|
||||
if _error:
|
||||
if _error is Array and _error.size() > 0 and _error[0].has("error"):
|
||||
_error = _error[0].error
|
||||
elif _error is Dictionary and _error.keys().size() > 0 and _error.has("error"):
|
||||
_error = _error.error
|
||||
|
||||
error = _error
|
||||
else:
|
||||
#error.code, error.status, error.message
|
||||
error = { "error": {
|
||||
"code": 0,
|
||||
"status": "Unknown Error",
|
||||
"message": "Error: %s - %s" % [action, description]
|
||||
}
|
||||
}
|
||||
|
||||
data = null
|
||||
|
||||
func set_action(value : int) -> void:
|
||||
action = value
|
||||
match action:
|
||||
Task.TASK_GET, Task.TASK_LIST:
|
||||
_method = HTTPClient.METHOD_GET
|
||||
Task.TASK_POST, Task.TASK_QUERY, Task.TASK_AGG_QUERY:
|
||||
_method = HTTPClient.METHOD_POST
|
||||
Task.TASK_PATCH:
|
||||
_method = HTTPClient.METHOD_PATCH
|
||||
Task.TASK_DELETE:
|
||||
_method = HTTPClient.METHOD_DELETE
|
||||
Task.TASK_COMMIT:
|
||||
_method = HTTPClient.METHOD_POST
|
||||
_:
|
||||
assert(false)
|
||||
|
||||
|
||||
func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary:
|
||||
var ret := dic_a.duplicate(true)
|
||||
for key in dic_b:
|
||||
var val = dic_b[key]
|
||||
|
||||
if val == null and nullify:
|
||||
ret.erase(key)
|
||||
elif val is Array:
|
||||
ret[key] = _merge_array(ret.get(key) if ret.get(key) else [], val)
|
||||
elif val is Dictionary:
|
||||
ret[key] = _merge_dict(ret.get(key) if ret.get(key) else {}, val)
|
||||
else:
|
||||
ret[key] = val
|
||||
return ret
|
||||
|
||||
|
||||
func _merge_array(arr_a : Array, arr_b : Array, nullify := false) -> Array:
|
||||
var ret := arr_a.duplicate(true)
|
||||
ret.resize(len(arr_b))
|
||||
|
||||
var deletions := 0
|
||||
for i in len(arr_b):
|
||||
var index : int = i - deletions
|
||||
var val = arr_b[index]
|
||||
if val == null and nullify:
|
||||
ret.remove_at(index)
|
||||
deletions += i
|
||||
elif val is Array:
|
||||
ret[index] = _merge_array(ret[index] if ret[index] else [], val)
|
||||
elif val is Dictionary:
|
||||
ret[index] = _merge_dict(ret[index] if ret[index] else {}, val)
|
||||
else:
|
||||
ret[index] = val
|
||||
return ret
|
||||
1
addons/godot-firebase/firestore/firestore_task.gd.uid
Normal file
1
addons/godot-firebase/firestore/firestore_task.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bacq4pvag0wii
|
||||
3
addons/godot-firebase/firestore/firestore_transform.gd
Normal file
3
addons/godot-firebase/firestore/firestore_transform.gd
Normal file
@@ -0,0 +1,3 @@
|
||||
class_name FirestoreTransform
|
||||
extends RefCounted
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://pybqqmlkru0q
|
||||
Reference in New Issue
Block a user