Skip to content

Commit 6d32fbe

Browse files
committed
Add ability to do aggregation queries
1 parent 024ef50 commit 6d32fbe

File tree

3 files changed

+108
-109
lines changed

3 files changed

+108
-109
lines changed

addons/godot-firebase/firestore/firestore.gd

+52-85
Original file line numberDiff line numberDiff line change
@@ -40,39 +40,27 @@ const _MAX_POOLED_REQUEST_AGE = 30
4040
## The code indicating the request Firestore is processing.
4141
## See @[enum FirebaseFirestore.Requests] to get a full list of codes identifiers.
4242
## @enum Requests
43-
var request : int = -1
44-
45-
## Whether cache files can be used and generated.
46-
## @default true
47-
var persistence_enabled : bool = false
48-
49-
## Whether an internet connection can be used.
50-
## @default true
51-
var networking: bool = true : set = set_networking
43+
var request: int = -1
5244

5345
## A Dictionary containing all authentication fields for the current logged user.
5446
## @type Dictionary
55-
var auth : Dictionary
47+
var auth: Dictionary
5648

57-
var _config : Dictionary = {}
49+
var _config: Dictionary = {}
5850
var _cache_loc: String
5951
var _encrypt_key := "5vg76n90345f7w390346" if Utilities.is_web() else OS.get_unique_id()
6052

6153

62-
var _base_url : String = ""
63-
var _extended_url : String = "projects/[PROJECT_ID]/databases/(default)/documents/"
64-
var _query_suffix : String = ":runQuery"
54+
var _base_url: String = ""
55+
var _extended_url: String = "projects/[PROJECT_ID]/databases/(default)/documents/"
56+
var _query_suffix: String = ":runQuery"
57+
var _agg_query_suffix: String = ":runAggregationQuery"
6558

6659
#var _connect_check_node : HTTPRequest
6760

68-
var _request_list_node : HTTPRequest
69-
var _requests_queue : Array = []
70-
var _current_query : FirestoreQuery
71-
72-
var _offline: bool = false : set = _set_offline
73-
74-
func _ready() -> void:
75-
pass
61+
var _request_list_node: HTTPRequest
62+
var _requests_queue: Array = []
63+
var _current_query: FirestoreQuery
7664

7765
## Returns a reference collection by its [i]path[/i].
7866
##
@@ -99,49 +87,69 @@ func collection(path : String) -> FirestoreCollection:
9987
## Issue a query checked your Firestore database.
10088
##
10189
## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query.
102-
## This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued.
103-
## If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal.
104-
##
105-
## ex.
106-
## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code]
107-
## [code]await query_task.task_finished[/code]
108-
## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function.
90+
## When awaited, this function returns the resulting array from the query.
10991
##
11092
## ex.
111-
## [code]var result : Array = await query_task.task_finished[/code]
93+
## [code]var query_results = wait Firebase.Firestore.query(FirestoreQuery.new())[/code]
11294
##
11395
## [b]Warning:[/b] It currently does not work offline!
11496
##
11597
## @args query
11698
## @arg-types FirestoreQuery
117-
## @return FirestoreTask
99+
## @return Array[FirestoreDocument]
118100
func query(query : FirestoreQuery) -> Array:
101+
if query.aggregations.size() > 0:
102+
Firebase._printerr("Aggregation query sent with normal query call: " + str(query))
103+
return []
104+
119105
var task : FirestoreTask = FirestoreTask.new()
120106
task.action = FirestoreTask.Task.TASK_QUERY
121-
var body : Dictionary = { structuredQuery = query.query }
122-
var url : String = _base_url + _extended_url + _query_suffix
123-
107+
var body: Dictionary = { structuredQuery = query.query }
108+
var url: String = _base_url + _extended_url + _query_suffix
109+
124110
task.data = query
125111
task._fields = JSON.stringify(body)
126112
task._url = url
127113
_pooled_request(task)
128114
return await _handle_task_finished(task)
129-
130-
131-
## Request a list of contents (documents and/or collections) inside a collection, specified by its [i]id[/i]. This method will return a [code]FirestoreTask[/code] object, representing a reference to the request issued. If saved into a variable, the [code]FirestoreTask[/code] object can be used to yield checked the [code]result_query(result)[/code] signal, or the more generic [code]task_finished(result)[/code] signal.
132-
## [b]Note:[/b] [code]order_by[/code] does not work in offline mode.
133-
## ex.
134-
## [code]var query_task : FirestoreTask = Firebase.Firestore.query(FirestoreQuery.new())[/code]
135-
## [code]await query_task.task_finished[/code]
136-
## Since the emitted signal is holding an argument, it can be directly retrieved as a return variable from the [code]yield()[/code] function.
115+
116+
## Issue an aggregation query (sum, average, count) against your Firestore database;
117+
## cheaper than a normal query and counting (for instance) values directly.
118+
##
119+
## [b]Note:[/b] a [code]FirestoreQuery[/code] object needs to be created to issue the query.
120+
## When awaited, this function returns the result from the aggregation query.
137121
##
138122
## ex.
139-
## [code]var result : Array = await query_task.task_finished[/code]
123+
## [code]var query_results = await Firebase.Firestore.query(FirestoreQuery.new())[/code]
124+
##
125+
## [b]Warning:[/b] It currently does not work offline!
140126
##
127+
## @args query
128+
## @arg-types FirestoreQuery
129+
## @return Variant representing the array results of the aggregation query
130+
func aggregation_query(query : FirestoreQuery) -> Variant:
131+
if query.aggregations.size() == 0:
132+
Firebase._printerr("Aggregation query sent with no aggregation values: " + str(query))
133+
return 0
134+
135+
var task : FirestoreTask = FirestoreTask.new()
136+
task.action = FirestoreTask.Task.TASK_AGG_QUERY
137+
138+
var body: Dictionary = { structuredAggregationQuery = { structuredQuery = query.query, aggregations = query.aggregations } }
139+
var url: String = _base_url + _extended_url + _agg_query_suffix
140+
141+
task.data = query
142+
task._fields = JSON.stringify(body)
143+
task._url = url
144+
_pooled_request(task)
145+
var result = await _handle_task_finished(task)
146+
return result
147+
148+
## 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]
141149
## @args collection_id, page_size, page_token, order_by
142150
## @arg-types String, int, String, String
143151
## @arg-defaults , 0, "", ""
144-
## @return FirestoreTask
152+
## @return Array[FirestoreDocument]
145153
func list(path : String = "", page_size : int = 0, page_token : String = "", order_by : String = "") -> Array:
146154
var task : FirestoreTask = FirestoreTask.new()
147155
task.action = FirestoreTask.Task.TASK_LIST
@@ -160,38 +168,6 @@ func list(path : String = "", page_size : int = 0, page_token : String = "", ord
160168
return await _handle_task_finished(task)
161169

162170

163-
func set_networking(value: bool) -> void:
164-
if value:
165-
enable_networking()
166-
else:
167-
disable_networking()
168-
169-
170-
func enable_networking() -> void:
171-
if networking:
172-
return
173-
networking = true
174-
_base_url = _base_url.replace("storeoffline", "firestore")
175-
for coll in get_children():
176-
if coll is FirestoreCollection:
177-
coll._base_url = _base_url
178-
179-
180-
func disable_networking() -> void:
181-
if not networking:
182-
return
183-
networking = false
184-
# Pointing to an invalid url should do the trick.
185-
_base_url = _base_url.replace("firestore", "storeoffline")
186-
for coll in get_children():
187-
if coll is FirestoreCollection:
188-
coll._base_url = _base_url
189-
190-
191-
func _set_offline(value: bool) -> void:
192-
return # Since caching is causing a lot of issues, I'm turning it off for now. We will revisit this in the future, once we have some time to investigate why the cache is being corrupted.
193-
194-
195171
func _set_config(config_json : Dictionary) -> void:
196172
_config = config_json
197173
_cache_loc = _config["cacheLocation"]
@@ -213,10 +189,6 @@ func _check_emulating() -> void :
213189
_base_url = "http://localhost:{port}/{version}/".format({ version = _API_VERSION, port = port })
214190

215191
func _pooled_request(task : FirestoreTask) -> void:
216-
if _offline:
217-
task._on_request_completed(HTTPRequest.RESULT_CANT_CONNECT, 404, PackedStringArray(), PackedByteArray())
218-
return
219-
220192
if (auth == null or auth.is_empty()) and not Firebase.emulating:
221193
Firebase._print("Unauthenticated request issued...")
222194
Firebase.Auth.login_anonymous()
@@ -252,11 +224,6 @@ func _on_FirebaseAuth_token_refresh_succeeded(auth_result : Dictionary) -> void:
252224
if coll is FirestoreCollection:
253225
coll.auth = auth
254226

255-
func _on_connect_check_request_completed(result : int, _response_code, _headers, _body) -> void:
256-
_set_offline(result != HTTPRequest.RESULT_SUCCESS)
257-
#_connect_check_node.request(_base_url)
258-
259-
260227
func _on_FirebaseAuth_logout() -> void:
261228
auth = {}
262229

addons/godot-firebase/firestore/firestore_query.gd

+28-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## @meta-authors Nicoló 'fenix' Santilio
1+
## @meta-authors Nicoló 'fenix' Santilio, Kyle Szklenski
22
## @meta-version 1.4
33
## A firestore query.
44
## Documentation TODO.
@@ -7,19 +7,19 @@ extends RefCounted
77
class_name FirestoreQuery
88

99
class Order:
10-
var obj : Dictionary
10+
var obj: Dictionary
1111

1212
class Cursor:
13-
var values : Array
14-
var before : bool
13+
var values: Array
14+
var before: bool
1515

1616
func _init(v : Array,b : bool):
1717
values = v
1818
before = b
1919

2020
signal query_result(query_result)
2121

22-
const TEMPLATE_QUERY : Dictionary = {
22+
const TEMPLATE_QUERY: Dictionary = {
2323
select = {},
2424
from = [],
2525
where = {},
@@ -30,11 +30,12 @@ const TEMPLATE_QUERY : Dictionary = {
3030
limit = 0
3131
}
3232

33-
var query : Dictionary = {}
33+
var query: Dictionary = {}
34+
var aggregations: Array[Dictionary] = []
3435

3536
enum OPERATOR {
3637
# Standard operators
37-
OPERATOR_NSPECIFIED,
38+
OPERATOR_UNSPECIFIED,
3839
LESS_THAN,
3940
LESS_THAN_OR_EQUAL,
4041
GREATER_THAN,
@@ -87,8 +88,6 @@ func from(collection_id : String, all_descendants : bool = true) -> FirestoreQue
8788
query["from"] = [{collectionId = collection_id, allDescendants = all_descendants}]
8889
return self
8990

90-
91-
9291
# @collections_array MUST be an Array of Arrays with this structure
9392
# [ ["collection_id", true/false] ]
9493
func from_many(collections_array : Array) -> FirestoreQuery:
@@ -159,8 +158,6 @@ func order_by_fields(order_field_list : Array) -> FirestoreQuery:
159158
query["orderBy"] = order_list
160159
return self
161160

162-
163-
164161
func start_at(value, before : bool) -> FirestoreQuery:
165162
var cursor : Cursor = _cursor_object(value, before)
166163
query["startAt"] = { values = cursor.values, before = cursor.before }
@@ -191,6 +188,26 @@ func limit(limit : int) -> FirestoreQuery:
191188
return self
192189

193190

191+
func aggregate() -> FirestoreAggregation:
192+
return FirestoreAggregation.new(self)
193+
194+
class FirestoreAggregation extends RefCounted:
195+
var _query: FirestoreQuery
196+
197+
func _init(query: FirestoreQuery) -> void:
198+
_query = query
199+
200+
func sum(field: String) -> FirestoreQuery:
201+
_query.aggregations.push_back({ sum = { field = { fieldPath = field }}})
202+
return _query
203+
204+
func count(up_to: int) -> FirestoreQuery:
205+
_query.aggregations.push_back({ count = { upTo = up_to }})
206+
return _query
207+
208+
func average(field: String) -> FirestoreQuery:
209+
_query.aggregations.push_back({ avg = { field = { fieldPath = field }}})
210+
return _query
194211

195212
# UTILITIES ----------------------------------------
196213

addons/godot-firebase/firestore/firestore_task.gd

+28-13
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ enum Task {
3131
TASK_PATCH, ## A PATCH Request Task, processing a update() request
3232
TASK_DELETE, ## A DELETE Request Task, processing a delete() request
3333
TASK_QUERY, ## A POST Request Task, processing a query() request
34+
TASK_AGG_QUERY, ## A POST Request Task, processing an aggregation_query() request
3435
TASK_LIST, ## A POST Request Task, processing a list() request
3536
TASK_COMMIT ## A POST Request Task that hits the write api
3637
}
@@ -43,7 +44,8 @@ const TASK_MAP = {
4344
Task.TASK_DELETE: "DELETE DOCUMENT",
4445
Task.TASK_QUERY: "QUERY COLLECTION",
4546
Task.TASK_LIST: "LIST DOCUMENTS",
46-
Task.TASK_COMMIT: "COMMIT DOCUMENT"
47+
Task.TASK_COMMIT: "COMMIT DOCUMENT",
48+
Task.TASK_AGG_QUERY: "AGG QUERY COLLECTION"
4749
}
4850

4951
## The code indicating the request Firestore is processing.
@@ -53,24 +55,23 @@ var action : int = -1 : set = set_action
5355

5456
## A variable, temporary holding the result of the request.
5557
var data
56-
var error : Dictionary
57-
var document : FirestoreDocument
58+
var error: Dictionary
59+
var document: FirestoreDocument
5860

59-
var _response_headers : PackedStringArray = PackedStringArray()
60-
var _response_code : int = 0
61+
var _response_headers: PackedStringArray = PackedStringArray()
62+
var _response_code: int = 0
6163

62-
var _method : int = -1
63-
var _url : String = ""
64-
var _fields : String = ""
65-
var _headers : PackedStringArray = []
64+
var _method: int = -1
65+
var _url: String = ""
66+
var _fields: String = ""
67+
var _headers: PackedStringArray = []
6668

67-
func _on_request_completed(result : int, response_code : int, headers : PackedStringArray, body : PackedByteArray) -> void:
69+
func _on_request_completed(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray) -> void:
6870
var bod = body.get_string_from_utf8()
6971
if bod != "":
7072
bod = Utilities.get_json_data(bod)
71-
73+
7274
var failed: bool = bod is Dictionary and bod.has("error") and response_code != HTTPClient.RESPONSE_OK
73-
7475
# Probably going to regret this...
7576
if response_code == HTTPClient.RESPONSE_OK:
7677
match action:
@@ -84,6 +85,18 @@ func _on_request_completed(result : int, response_code : int, headers : PackedSt
8485
for doc in bod:
8586
if doc.has('document'):
8687
data.append(FirestoreDocument.new(doc.document))
88+
Task.TASK_AGG_QUERY:
89+
var agg_results = []
90+
for agg_result in bod:
91+
var idx = 0
92+
var query_results = {}
93+
for field_value in agg_result.result.aggregateFields.keys():
94+
var agg = data.aggregations[idx]
95+
var field = agg_result.result.aggregateFields[field_value]
96+
query_results[agg.keys()[0]] = Utilities.from_firebase_type(field)
97+
idx += 1
98+
agg_results.push_back(query_results)
99+
data = agg_results
87100
Task.TASK_LIST:
88101
data = []
89102
if bod.has('documents'):
@@ -127,14 +140,16 @@ func set_action(value : int) -> void:
127140
match action:
128141
Task.TASK_GET, Task.TASK_LIST:
129142
_method = HTTPClient.METHOD_GET
130-
Task.TASK_POST, Task.TASK_QUERY:
143+
Task.TASK_POST, Task.TASK_QUERY, Task.TASK_AGG_QUERY:
131144
_method = HTTPClient.METHOD_POST
132145
Task.TASK_PATCH:
133146
_method = HTTPClient.METHOD_PATCH
134147
Task.TASK_DELETE:
135148
_method = HTTPClient.METHOD_DELETE
136149
Task.TASK_COMMIT:
137150
_method = HTTPClient.METHOD_POST
151+
_:
152+
assert(false)
138153

139154

140155
func _merge_dict(dic_a : Dictionary, dic_b : Dictionary, nullify := false) -> Dictionary:

0 commit comments

Comments
 (0)