Skip to content

Commit 6951e69

Browse files
feat(firestore): support for complex queries
1 parent 89fb93a commit 6951e69

File tree

1 file changed

+317
-10
lines changed

1 file changed

+317
-10
lines changed

firebase/firestore/__init__.py

Lines changed: 317 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from proto.message import Message
1717
from google.cloud.firestore import Client
1818
from google.cloud.firestore_v1._helpers import *
19+
from google.cloud.firestore_v1.query import Query
20+
from google.cloud.firestore_v1.collection import CollectionReference
21+
from google.cloud.firestore_v1.base_query import _enum_from_direction
1922

2023
from ._utils import _from_datastore, _to_datastore
2124
from firebase._exception import raise_detailed_error
@@ -96,6 +99,64 @@ def __init__(self, collection_path, api_key, credentials, project_id, requests):
9699
if self._credentials:
97100
self.__datastore = Client(credentials=self._credentials, project=self._project_id)
98101

102+
self._query = {}
103+
self._is_limited_to_last = False
104+
105+
def _build_query(self):
106+
""" Builds query for firestore to execute.
107+
108+
109+
:return: An query.
110+
:rtype: :class:`~google.cloud.firestore_v1.query.Query`
111+
"""
112+
113+
if self._credentials:
114+
_query = _build_db(self.__datastore, self._path)
115+
else:
116+
_query = Query(CollectionReference(self._path.pop()))
117+
118+
for key, val in self._query.items():
119+
if key == 'endAt':
120+
_query = _query.end_at(val)
121+
elif key == 'endBefore':
122+
_query = _query.end_before(val)
123+
elif key == 'limit':
124+
_query = _query.limit(val)
125+
elif key == 'limitToLast':
126+
_query = _query.limit_to_last(val)
127+
elif key == 'offset':
128+
_query = _query.offset(val)
129+
elif key == 'orderBy':
130+
for q in val:
131+
_query = _query.order_by(q[0], **q[1])
132+
elif key == 'select':
133+
_query = _query.select(val)
134+
elif key == 'startAfter':
135+
_query = _query.start_after(val)
136+
elif key == 'startAt':
137+
_query = _query.start_at(val)
138+
elif key == 'where':
139+
for q in val:
140+
_query = _query.where(q[0], q[1], q[2])
141+
142+
if not self._credentials and _query._limit_to_last:
143+
144+
self._is_limited_to_last = _query._limit_to_last
145+
146+
for order in _query._orders:
147+
order.direction = _enum_from_direction(
148+
_query.DESCENDING
149+
if order.direction == _query.ASCENDING
150+
else _query.ASCENDING
151+
)
152+
153+
_query._limit_to_last = False
154+
155+
self._path.clear()
156+
self._query.clear()
157+
158+
return _query
159+
99160
def add(self, data, token=None):
100161
""" Create a document in the Firestore database with the
101162
provided data using an auto generated ID for the document.
@@ -155,6 +216,42 @@ def document(self, document_id):
155216
self._path.append(document_id)
156217
return Document(self._path, api_key=self._api_key, credentials=self._credentials, project_id=self._project_id, requests=self._requests)
157218

219+
def end_at(self, document_fields):
220+
""" End query at a cursor with this collection as parent.
221+
222+
223+
:type document_fields: dict
224+
:param document_fields: A dictionary of fields representing a
225+
query results cursor. A cursor is a collection of values
226+
that represent a position in a query result set.
227+
228+
229+
:return: A reference to the instance object.
230+
:rtype: Collection
231+
"""
232+
233+
self._query['endAt'] = document_fields
234+
235+
return self
236+
237+
def end_before(self, document_fields):
238+
""" End query before a cursor with this collection as parent.
239+
240+
241+
:type document_fields: dict
242+
:param document_fields: A dictionary of fields representing a
243+
query results cursor. A cursor is a collection of values
244+
that represent a position in a query result set.
245+
246+
247+
:return: A reference to the instance object.
248+
:rtype: Collection
249+
"""
250+
251+
self._query['endBefore'] = document_fields
252+
253+
return self
254+
158255
def get(self, token=None):
159256
""" Returns a list of dict's containing document ID and the
160257
data stored within them.
@@ -169,13 +266,10 @@ def get(self, token=None):
169266
:rtype: list
170267
"""
171268

172-
path = self._path.copy()
173-
self._path.clear()
174-
175269
docs = []
176270

177271
if self._credentials:
178-
db_ref = _build_db(self.__datastore, path)
272+
db_ref = self._build_query()
179273

180274
results = db_ref.get()
181275

@@ -184,23 +278,236 @@ def get(self, token=None):
184278

185279
else:
186280

187-
req_ref = f"{self._base_url}/{'/'.join(path)}?key={self._api_key}"
281+
body = None
282+
283+
if len(self._query) > 0:
284+
req_ref = f"{self._base_url}/{'/'.join(self._path[:-1])}:runQuery?key={self._api_key}"
285+
286+
body = {
287+
"structuredQuery": json.loads(Message.to_json(self._build_query()._to_protobuf()))
288+
}
289+
290+
else:
291+
req_ref = f"{self._base_url}/{'/'.join(self._path)}?key={self._api_key}"
188292

189293
if token:
190294
headers = {"Authorization": "Firebase " + token}
191-
response = self._requests.get(req_ref, headers=headers)
295+
296+
if body:
297+
response = self._requests.post(req_ref, headers=headers, json=body)
298+
else:
299+
response = self._requests.get(req_ref, headers=headers)
192300

193301
else:
194-
response = self._requests.get(req_ref)
302+
303+
if body:
304+
response = self._requests.post(req_ref, json=body)
305+
else:
306+
response = self._requests.get(req_ref)
195307

196308
raise_detailed_error(response)
197309

198-
for doc in response.json()['documents']:
199-
doc_id = doc['name'].split('/')
200-
docs.append({doc_id.pop(): _from_datastore({'fields': doc['fields']})})
310+
if isinstance(response.json(), dict):
311+
for doc in response.json()['documents']:
312+
doc_id = doc['name'].split('/')
313+
docs.append({doc_id.pop(): _from_datastore({'fields': doc['fields']})})
314+
315+
elif isinstance(response.json(), list):
316+
for doc in response.json():
317+
fields = {}
318+
319+
if doc.get('document'):
320+
321+
if doc.get('document').get('fields'):
322+
fields = doc['document']['fields']
323+
324+
doc_id = doc['document']['name'].split('/')
325+
docs.append({doc_id.pop(): _from_datastore({'fields': fields})})
326+
327+
if self._is_limited_to_last:
328+
docs = list(reversed(list(docs)))
201329

202330
return docs
203331

332+
def limit_to_first(self, count):
333+
""" Create a limited query with this collection as parent.
334+
335+
.. note::
336+
`limit_to_first` and `limit_to_last` are mutually
337+
exclusive. Setting `limit_to_first` will drop
338+
previously set `limit_to_last`.
339+
340+
341+
:type count: int
342+
:param count: Maximum number of documents to return that match
343+
the query.
344+
345+
346+
:return: A reference to the instance object.
347+
:rtype: Collection
348+
"""
349+
350+
self._query['limit'] = count
351+
352+
return self
353+
354+
def limit_to_last(self, count):
355+
""" Create a limited to last query with this collection as
356+
parent.
357+
358+
.. note::
359+
`limit_to_first` and `limit_to_last` are mutually
360+
exclusive. Setting `limit_to_first` will drop
361+
previously set `limit_to_last`.
362+
363+
364+
:type count: int
365+
:param count: Maximum number of documents to return that
366+
match the query.
367+
368+
369+
:return: A reference to the instance object.
370+
:rtype: Collection
371+
"""
372+
373+
self._query['limitToLast'] = count
374+
375+
return self
376+
377+
def offset(self, num_to_skip):
378+
""" Skip to an offset in a query with this collection as parent.
379+
380+
381+
:type num_to_skip: int
382+
:param num_to_skip: The number of results to skip at the
383+
beginning of query results. (Must be non-negative.)
384+
385+
386+
:return: A reference to the instance object.
387+
:rtype: Collection
388+
"""
389+
390+
self._query['offset'] = num_to_skip
391+
392+
return self
393+
394+
def order_by(self, field_path, **kwargs):
395+
""" Create an "order by" query with this collection as parent.
396+
397+
398+
:type field_path: str
399+
:param field_path: A field path (``.``-delimited list of field
400+
names) on which to order the query results.
401+
402+
:Keyword Arguments:
403+
* *direction* ( :class:`str` ) --
404+
Sort query results in ascending/descending order on a field.
405+
406+
407+
:return: A reference to the instance object.
408+
:rtype: Collection
409+
"""
410+
411+
arr = []
412+
413+
if self._query.get('orderBy'):
414+
arr = self._query['orderBy']
415+
416+
arr.append([field_path, kwargs])
417+
418+
self._query['orderBy'] = arr
419+
420+
return self
421+
422+
def select(self, field_paths):
423+
""" Create a "select" query with this collection as parent.
424+
425+
:type field_paths: list
426+
:param field_paths: A list of field paths (``.``-delimited list
427+
of field names) to use as a projection of document fields
428+
in the query results.
429+
430+
431+
:return: A reference to the instance object.
432+
:rtype: Collection
433+
"""
434+
435+
self._query['select'] = field_paths
436+
437+
return self
438+
439+
def start_after(self, document_fields):
440+
""" Start query after a cursor with this collection as parent.
441+
442+
443+
:type document_fields: dict
444+
:param document_fields: A dictionary of fields representing
445+
a query results cursor. A cursor is a collection of values
446+
that represent a position in a query result set.
447+
448+
449+
:return: A reference to the instance object.
450+
:rtype: Collection
451+
"""
452+
453+
self._query['startAfter'] = document_fields
454+
455+
return self
456+
457+
def start_at(self, document_fields):
458+
""" Start query at a cursor with this collection as parent.
459+
460+
461+
:type document_fields: dict
462+
:param document_fields: A dictionary of fields representing a
463+
query results cursor. A cursor is a collection of values
464+
that represent a position in a query result set.
465+
466+
467+
:return: A reference to the instance object.
468+
:rtype: Collection
469+
"""
470+
471+
self._query['startAt'] = document_fields
472+
473+
return self
474+
475+
def where(self, field_path, op_string, value):
476+
""" Create a "where" query with this collection as parent.
477+
478+
479+
:type field_path: str
480+
:param field_path: A field path (``.``-delimited list of field
481+
names) for the field to filter on.
482+
483+
:type op_string: str
484+
:param op_string: A comparison operation in the form of a
485+
string. Acceptable values are ``<``, ``<=``, ``==``, ``!=``
486+
, ``>=``, ``>``, ``in``, ``not-in``, ``array_contains`` and
487+
``array_contains_any``.
488+
489+
:type value: Any
490+
:param value: The value to compare the field against in the
491+
filter. If ``value`` is :data:`None` or a NaN, then ``==``
492+
is the only allowed operation. If ``op_string`` is ``in``,
493+
``value`` must be a sequence of values.
494+
495+
496+
:return: A reference to the instance object.
497+
:rtype: Collection
498+
"""
499+
500+
arr = []
501+
502+
if self._query.get('where'):
503+
arr = self._query['where']
504+
505+
arr.append([field_path, op_string, value])
506+
507+
self._query['where'] = arr
508+
509+
return self
510+
204511

205512
class Document:
206513
""" A reference to a document in a Firestore database.

0 commit comments

Comments
 (0)