Timestamp from firestore gets converted to a Map when using cloud function

3,975

Solution 1

If you are dealing with a Timestamp that's been serialized as an object with seconds and nanoseconds components, you can use those components to create a new Timestamp object with new Timestamp(seconds, nanoseconds).

Solution 2

Indeed, timestamps gets returned as plain Map when using Cloud functions. But if you use Firebase SDK it returns Timestamp object. I use the following function to handle both cases:

/// https://stackoverflow.com/a/57865272/1321917
DateTime dateTimeFromTimestamp(dynamic val) {
  Timestamp timestamp;
  if (val is Timestamp) {
    timestamp = val;
  } else if (val is Map) {
    timestamp = Timestamp(val['_seconds'], val['_nanoseconds']);
  }
  if (timestamp != null) {
    return timestamp.toDate();
  } else {
    print('Unable to parse Timestamp from $val');
    return null;
  }
}

Works perfectly with json_annotation lib:

  @JsonKey(
      fromJson: dateTimeFromTimestamp,
      toJson: dateTimeToTimestamp,
      nullable: true)
  final DateTime subscriptionExpiryDate;

Solution 3

You can use converted to receive DateTime like this:

class TimestampConverter implements JsonConverter<DateTime, dynamic> {
  const TimestampConverter();

  @override
  DateTime fromJson(dynamic data) {
    Timestamp timestamp;
    if (data is Timestamp) {
      timestamp = data;
    } else if (data is Map) {
      timestamp = Timestamp(data['_seconds'], data['_nanoseconds']);
    }
    return timestamp?.toDate();
  }

  @override
  Map<String, dynamic> toJson(DateTime dateTime) {
    final timestamp = Timestamp.fromDate(dateTime);
    return {
      '_seconds': timestamp.seconds,
      '_nanoseconds': timestamp.nanoseconds,
    };
  }
}

then mark your field of your model like that:

@TimestampConverter() DateTime createdAt

Solution 4

Andrey's answer is pretty good. Here's a JS/ Typescript adaptation, wrapped in a class:

import app from 'firebase/app'
import 'firebase/firestore'

import TimeAgo from 'javascript-time-ago'
// Load locale-specific relative date/time formatting rules.
import en from 'javascript-time-ago/locale/en'

// Add locale-specific relative date/time formatting rules.
TimeAgo.addLocale(en)

// Adapted from Andrey Gordeev's answer at: 
// https://stackoverflow.com/questions/56245156/timestamp-from-firestore-gets-converted-to-a-map-when-using-cloud-function

class MyClass {
    timeAgo: TimeAgo

    constructor() {
        this.timeAgo = new TimeAgo('en-US')
    }

    getTimeText = (timeObject: any) => {
        // Convert to time text once it's of type firestore.Timestamp
        const getTextFromTimestamp = (timestamp: app.firestore.Timestamp) => {
            return this.timeAgo.format(timestamp.toDate())

        }
        if (timeObject instanceof app.firestore.Timestamp) {
            // Check if Timestamp (accessed from client SDK)
            return getTextFromTimestamp(timeObject)
        } else if (Object.prototype.toString.call(timeObject) === '[object Object]') {
            // Check if it's a Map
            const seconds = timeObject['_seconds']
            const nanoseconds = timeObject['_nanoseconds']
            if (seconds && nanoseconds) {
                const timestamp = new app.firestore.Timestamp(seconds, nanoseconds)
                return getTextFromTimestamp(timestamp)
            }
        }
        console.log('Couldn\'t parse time', timeObject)
        // Fallback
        return 'some time ago'
    }
}
Share:
3,975
Me BMan
Author by

Me BMan

Updated on December 11, 2022

Comments

  • Me BMan
    Me BMan over 1 year

    So I have a Timestamp in cloud firestore. I am using cloud function to retrieve data from firestore to flutter. But JSON formats timestamp to map due to which I am not able to use it as timestamp. How to convert it again into timestamp?
    This is how I ulpoaded timestamp to firestore.

    var reference = Firestore.instance.collection('posts');
          reference.add({
            'postTitle': this.title,
            'timestamp': DateTime.now(),
            'likes': {},
            'ownerId': userId,
          })
    

    To retrieve data this is the code:

     factory Post.fromJSON(Map data){
        return Post(
          timestamp: data['timestamp'],
        );
      }
    
    List<Post> _generateFeed(List<Map<String, dynamic>> feedData) {
        List<Post> listOfPosts = [];
    
        for (var postData in feedData) {
          listOfPosts.add(Post.fromJSON(postData));
        }
    
        return listOfPosts;
      }
    

    but this returns an error.

    I/flutter (17271): The following assertion was thrown building FutureBuilder<DocumentSnapshot>(dirty, state:
    I/flutter (17271): _FutureBuilderState<DocumentSnapshot>#1536b):
    I/flutter (17271): type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'Timestamp'
    

    This is my cloud function.
    getFeed.ts

    import * as functions from 'firebase-functions';
    import * as admin from 'firebase-admin';
    
    export const getFeedModule = function(req, res){
        const uid = String(req.query.uid);
    
        async function compileFeedPost(){
            const following = await getFollowing(uid, res)as any;
    
            let listOfPosts = await getAllPosts(following, res);
    
            listOfPosts = [].concat.apply([], listOfPosts);
    
            res.send(listOfPosts);
        }
    
        compileFeedPost().then().catch();
    }
    
    async function getAllPosts(following, res) {
        let listOfPosts = [];
    
        for (let user in following){
            listOfPosts.push( await getUserPosts(following[user], res));
        }
        return listOfPosts;
    }
    
    function getUserPosts(userId, res){
        const posts = admin.firestore().collection("posts").where("ownerId", "==", userId).orderBy("timestamp")
    
        return posts.get()
        .then(function(querySnapshot){
            let listOfPosts = [];
    
            querySnapshot.forEach(function(doc){
                listOfPosts.push(doc.data());
            });
    
            return listOfPosts;
        })
    }
    
    function getFollowing(uid, res){
        const doc = admin.firestore().doc(`user/${uid}`)
        return doc.get().then(snapshot => {
            const followings = snapshot.data().followings;
    
            let following_list = [];
    
            for (const following in followings){
                if (followings[following] === true){
                    following_list.push(following);
                }
            }
            return following_list;
        }).catch(error => {
            res.status(500).send(error)
        })
    }
    

    cloud function index.ts

    import * as functions from 'firebase-functions';
    import * as admin from 'firebase-admin';
    import { getFeedModule } from "./getFeed"
    admin.initializeApp();
    
    export const getFeed = functions.https.onRequest((req, res) => {
        getFeedModule(req, res);
    })
    
    
    

    invoked by this

    _getFeed() async {
        print("Starting getFeed");
        FirebaseUser user = await FirebaseAuth.instance.currentUser();
    
        SharedPreferences prefs = await SharedPreferences.getInstance();
    
        String userId = user.uid;
        var url =
            'https://us-central1-jaluk-quiz.cloudfunctions.net/getFeed?uid=' + userId;
        var httpClient = HttpClient();
    
        List<QuizViewer>listOfPosts;
        String result;
        try {
          var request = await httpClient.getUrl(Uri.parse(url));
          var response = await request.close(); 
          if (response.statusCode == HttpStatus.ok) {
            String json = await response.transform(utf8.decoder).join();
            prefs.setString("feed", json);
            List<Map<String, dynamic>> data =
                jsonDecode(json).cast<Map<String, dynamic>>();
            listOfPosts = _generateFeed(data);
            result = "Success in http request for feed";
          } else {
            result =
                'Error getting a feed: Http status ${response.statusCode} | userId $userId';
          }
        } catch (exception) {
          result = 'Failed invoking the getFeed function. Exception: $exception';
        }
        print(result);
    
        setState(() {
          feedData = listOfPosts;
        });
      }
    
  • Doug Stevenson
    Doug Stevenson almost 5 years
    I don't recommend storing dates as strings in Firestore (or any other database, for that matter. You should stick to the Timestamp, or if that doesn't work, use a single integer measured in unix epoch time.
  • Me BMan
    Me BMan almost 5 years
    Wow! that's genius. Thanks for the help guru! But the JSON is giving me in this format {_seconds: 1552543554634, _nanoseconds: 88000000}. How do I put it in Timestamp(seconds, nanoseconds)?
  • Doug Stevenson
    Doug Stevenson almost 5 years
    I don't actually do dart programming. You will have to figure out how to get those values out of your Cloud Functions response. It should be straightforward, if you are already accessing other parts of the response.
  • Me BMan
    Me BMan almost 5 years
    Oh ok, no problem! You showed me the way. I will try to put it in the timestamp. Thanks again!
  • Damien Romito
    Damien Romito almost 4 years
    @DougStevenson anyway to avoid this serialization from cloudfunction ? It's not conveniant to parse each JSON TIMESTAMP each time I return an object with timestamp from cloudfunction