├── .gitignore ├── README.md ├── requirements.txt └── src ├── __init__.py ├── configuration.py ├── firestore_triggers.py ├── helpers.py └── transactions.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firestore cloud functions (Python 3.7 Runtime) 2 | 3 | 4 | 5 | ### Introduction 6 | Cloud Firestore (In Beta at time of writing) is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud 7 | Platform. Like Firebase Realtime Database, it keeps your data in sync across client apps through realtime listeners and 8 | offers offline support for mobile and web so you can build responsive apps that work regardless of network latency or 9 | Internet connectivity. 10 | 11 | More info: https://firebase.google.com/docs/firestore/ 12 | 13 | 14 | use cases for cloud functions: 15 | 1. Notify users when something happens 16 | 2. realtime maintainance and cleanup 17 | 3. execute extensive tasks which cannot be done on the client 18 | 4. integrate with third party services 19 | 4. increment/decrement counters upon some event 20 | 21 | More info: https://firebase.google.com/docs/functions/use-cases 22 | 23 | ### Announcement 24 | Google finally announced support for python 3.7 runtime although the docs aren't updated yet. 25 | 26 | ### Scenerio 27 | Consider the following structure 28 | ``` 29 | finalDb/ 30 | account_1/ 31 | projects/ 32 | closedTaskCount (integer) 33 | createedTime (timestamp) 34 | creator (string) 35 | deleted (boolean) 36 | description (string) 37 | projectDueDate (timestamp) 38 | projectTags (map) 39 | startDate (timestamp) 40 | taskCount (integer) 41 | 42 | tasks/ 43 | attachments (map) 44 | createdTime (timestamp) 45 | creator (string) 46 | deleted (boolean) 47 | dueDate (timestamp) 48 | lastUpdatedTime (timestamp) 49 | project (string) 50 | owner (string) 51 | status (string) 52 | subTasks (map) 53 | tags (map) 54 | taskName (string) 55 | taskUsers (map) 56 | 57 | messages/ 58 | tags/ 59 | users/ 60 | activities/ 61 | ..... 62 | 63 | 64 | account_2/ 65 | account_3/ 66 | ..... 67 | account_n/ 68 | ``` 69 | 70 | ### Description 71 | 1. users can create tasks with 5 statuses : open, closed, pause, yet to start, suspended 72 | 2. A task can exist independently without being included in the project 73 | 3. closed task count is incremented when task changes its state from open to closed and decremented when its state changes 74 | from closed to open 75 | 4. taskcount is incremented/decremented whenever a new task is added or removed respectively 76 | 77 | 78 | ### Goal 79 | 80 | To increment/decrement the counters i.e; taskCount and closedTaskCount in projects collection whenever a task is 81 | created and references a project 82 | 83 | 84 | ### Reason to use cloud function 85 | To achieve consistency across all clients using transactions. 86 | Whenever a user makes changes, it must be consistent across all users and since transactions fail to execute whenever a 87 | device is offline, cloud functions are used to keep sanity with the value of counters when the device is online. 88 | 89 | More info on transactions: https://firebase.google.com/docs/firestore/manage-data/transactions 90 | 91 | 92 | ### Trigger a firestore cloud function 93 | The Cloud Functions for Firebase SDK exports a functions.firestore object that allows you to create handlers tied to specific events. 94 | Cloud Firestore supports create, update, delete, and write events. 95 | 96 | More info: https://firebase.google.com/docs/firestore/extend-with-functions 97 | 98 | 99 | ### Steps to create a cloud function in python 3.7 100 | 1. Goto google cloud console > cloud functions 101 | 2. Click on create function 102 | 3. Fill in function details such as function name, RAM. 103 | 4. Select appropriate Trigger (in this case: cloud firestore) 104 | 5. Select event type (create, update or delete or write) 105 | 6. Provide a Document path to listen to changes. 106 | In this case: finalDb/{orgId}/tasks/{taskId} 107 | 7. select source code type (inline editor/zip) 108 | 8. Select runtime as python 3.7 109 | 9. Provide the entry point i.e the function to start execution. 110 | 111 | **Note**: 112 | 1. Add firebase-admin==2.11.0 in requirements.txt file 113 | 114 | 115 | Refer my stack overflow answer for screenshots: 116 | https://stackoverflow.com/questions/48772583/python-in-google-cloud-functions/51466795#51466795 117 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | firebase-admin==2.11.0 -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adihat/firestore-cloud-functions/35b002bd6f07e24292f24f49957edc990ed9430e/src/__init__.py -------------------------------------------------------------------------------- /src/configuration.py: -------------------------------------------------------------------------------- 1 | import firebase_admin 2 | from firebase_admin import credentials, firestore 3 | 4 | PROJECT_NAME = 'your_google_project_name' 5 | 6 | # initialize firebase sdk 7 | CREDENTIALS = credentials.ApplicationDefault() 8 | firebase_admin.initialize_app(CREDENTIALS, { 9 | 'projectId': PROJECT_NAME, 10 | }) 11 | 12 | # get firestore client 13 | FIRESTORE_CLIENT = firestore.client() 14 | -------------------------------------------------------------------------------- /src/firestore_triggers.py: -------------------------------------------------------------------------------- 1 | from .helpers import get_old_task_status, get_new_task_status, prepare_project_reference, get_transaction_obj 2 | from .configuration import FIRESTORE_CLIENT 3 | from .transactions import update_task_count_in_transaction, update_closed_task_count_in_transaction, \ 4 | update_project_status_in_transaction 5 | 6 | 7 | def update_task_count(event, context): 8 | # prepare project reference 9 | project_ref = prepare_project_reference(event, context) 10 | 11 | # update the count using transaction 12 | transaction = get_transaction_obj() 13 | update_task_count_in_transaction(transaction, project_ref) 14 | 15 | return True 16 | 17 | 18 | def update_closed_task_count(event, context): 19 | # get old task status 20 | old_status = get_old_task_status(event) 21 | 22 | # get the new task status 23 | new_status = get_new_task_status(event) 24 | 25 | # prepare project reference 26 | project_ref = prepare_project_reference(event, context) 27 | 28 | # update the count using transaction 29 | transaction = get_transaction_obj() 30 | update_closed_task_count_in_transaction(transaction, project_ref, old_status, new_status) 31 | 32 | return True 33 | 34 | 35 | def update_project_on_task_deletion(event, context): 36 | # get old task status 37 | task_status = get_old_task_status(event) 38 | 39 | # prepare project reference 40 | project_ref = prepare_project_reference(event, context) 41 | 42 | # update the count using transaction 43 | transaction = get_transaction_obj() 44 | update_project_status_in_transaction(transaction, project_ref, task_status) 45 | 46 | return True 47 | -------------------------------------------------------------------------------- /src/helpers.py: -------------------------------------------------------------------------------- 1 | from .configuration import FIRESTORE_CLIENT 2 | 3 | 4 | def get_project_id(event): 5 | return event.get('value', {}).get('fields', {}).get('project', {}).get('stringValue', '') 6 | 7 | 8 | def get_project_id_on_deletion(event): 9 | return event.get('oldvalue', {}).get('fields', {}).get('project', {}).get('stringValue', '') 10 | 11 | 12 | def get_project_path(context, project_id): 13 | resource_string = context.resource 14 | return '/'.join(resource_string.split('/')[5:7] + ['projects', project_id]) 15 | 16 | 17 | def get_old_task_status(event): 18 | return event.get('oldValue', {}).get('fields', {}).get('status', {}).get('stringValue', '') 19 | 20 | 21 | def get_new_task_status(event): 22 | return event.get('value', {}).get('fields', {}).get('status', {}).get('stringValue', '') 23 | 24 | 25 | def get_transaction_obj(): 26 | return FIRESTORE_CLIENT.transaction() 27 | 28 | 29 | def prepare_project_reference(event, context): 30 | project_id = get_project_id(event) 31 | if not project_id: 32 | print('no project id') 33 | return True 34 | project_ref = get_project_path(context, project_id) 35 | return project_ref 36 | -------------------------------------------------------------------------------- /src/transactions.py: -------------------------------------------------------------------------------- 1 | from firebase_admin import firestore 2 | 3 | from .configuration import FIRESTORE_CLIENT 4 | 5 | 6 | @firestore.transactional 7 | def update_task_count_in_transaction(transaction, project_ref): 8 | doc_ref = FIRESTORE_CLIENT.document(project_ref) 9 | snapshot = doc_ref.get(transaction=transaction) 10 | transaction.update(doc_ref, { 11 | u'taskCount': snapshot.get(u'taskCount') + 1 12 | }) 13 | 14 | 15 | @firestore.transactional 16 | def update_closed_task_count_in_transaction(transaction, project_ref, old_status, new_status): 17 | doc_ref = FIRESTORE_CLIENT.document(project_ref) 18 | 19 | # get the document 20 | project = doc_ref.get().to_dict() 21 | 22 | if old_status == 'open' and new_status == 'closed': 23 | snapshot = doc_ref.get(transaction=transaction) 24 | transaction.update(doc_ref, { 25 | u'closedTaskCount': snapshot.get(u'closedTaskCount') + 1 26 | }) 27 | elif old_status == 'closed' and new_status == 'open': 28 | if project['closedTaskCount'] > 0: 29 | snapshot = doc_ref.get(transaction=transaction) 30 | transaction.update(doc_ref, { 31 | u'closedTaskCount': snapshot.get(u'closedTaskCount') - 1 32 | }) 33 | else: 34 | print('didnt match any condition') 35 | 36 | 37 | @firestore.transactional 38 | def update_project_status_in_transaction(transaction, project_ref, task_status): 39 | doc_ref = FIRESTORE_CLIENT.document(project_ref) 40 | 41 | # get the document 42 | project = doc_ref.get().to_dict() 43 | 44 | # decrement task count 45 | if project['taskCount'] > 0: 46 | snapshot = doc_ref.get(transaction=transaction) 47 | transaction.update(doc_ref, { 48 | u'taskCount': snapshot.get(u'taskCount') - 1 49 | }) 50 | 51 | # decrement closed task count 52 | if task_status == 'closed': 53 | if project['closedTaskCount'] > 0: 54 | snapshot = doc_ref.get(transaction=transaction) 55 | transaction.update(doc_ref, { 56 | u'closedTaskCount': snapshot.get(u'closedTaskCount') - 1 57 | }) 58 | --------------------------------------------------------------------------------