16
16
from proto .message import Message
17
17
from google .cloud .firestore import Client
18
18
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
19
22
20
23
from ._utils import _from_datastore , _to_datastore
21
24
from firebase ._exception import raise_detailed_error
@@ -96,6 +99,64 @@ def __init__(self, collection_path, api_key, credentials, project_id, requests):
96
99
if self ._credentials :
97
100
self .__datastore = Client (credentials = self ._credentials , project = self ._project_id )
98
101
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
+
99
160
def add (self , data , token = None ):
100
161
""" Create a document in the Firestore database with the
101
162
provided data using an auto generated ID for the document.
@@ -155,6 +216,42 @@ def document(self, document_id):
155
216
self ._path .append (document_id )
156
217
return Document (self ._path , api_key = self ._api_key , credentials = self ._credentials , project_id = self ._project_id , requests = self ._requests )
157
218
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
+
158
255
def get (self , token = None ):
159
256
""" Returns a list of dict's containing document ID and the
160
257
data stored within them.
@@ -169,13 +266,10 @@ def get(self, token=None):
169
266
:rtype: list
170
267
"""
171
268
172
- path = self ._path .copy ()
173
- self ._path .clear ()
174
-
175
269
docs = []
176
270
177
271
if self ._credentials :
178
- db_ref = _build_db ( self .__datastore , path )
272
+ db_ref = self ._build_query ( )
179
273
180
274
results = db_ref .get ()
181
275
@@ -184,23 +278,236 @@ def get(self, token=None):
184
278
185
279
else :
186
280
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 } "
188
292
189
293
if token :
190
294
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 )
192
300
193
301
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 )
195
307
196
308
raise_detailed_error (response )
197
309
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 )))
201
329
202
330
return docs
203
331
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
+
204
511
205
512
class Document :
206
513
""" A reference to a document in a Firestore database.
0 commit comments