6

I'm using a callable function from a client app to retrieve some data from firestore. The data is created using:

projectData = {
  created: firebase.firestore.FieldValue.serverTimestamp(),
  ...otherData
}
firebase.firestore().collection(projectsCollection).add(projectData)

And I can see the timestamp is correctly saved in the firestore console. The callable function does some other things and has error handling, but the data retrieval is done like this (using lodash to expand each document into an object to return to the client):

const projectRef = firestore.collection(gProjectsCollection).orderBy('created')
return projectRef.get().then(snapshot => {
  return {
    projects: _.chain(snapshot.docs)
      .keyBy('id')
      .mapValues(s => s.data())
      .value()
  }
})

This mostly works, but the returned object has a mangled created property (shown here in functions shell output):

RESPONSE RECEIVED FROM FUNCTION: 200, {
  "result": {
     "projects": {
      "XcRyQyaxLguRdbNmxQdn": {
        "name": "c",
        "description": "c",
        "created": {
          "_seconds": 1543405587,
          "_nanoseconds": 169000000
        }
      }
    }
  }
}

I'm presuming this is because the callable function uses JSON.stringify() internally, and the server timestamp isn't converted correctly. I tried explicitly converting the timestamp to a date like this:

return projectRef.get().then(snapshot => {
  return {
    exVersion,
    exId,
    projects: _.chain(snapshot.docs)
      .keyBy('id')
      .mapValues(s => s.data())
      .forEach(d => { d.created = d.created.toDate() })
      .value()
  }
})

but now I get an empty object back:

RESPONSE RECEIVED FROM FUNCTION: 200, {
  "result": {
    "projects": {
      "XcRyQyaxLguRdbNmxQdn": {
        "name": "c",
        "description": "c",
        "created": {}
      }
    }
  }
}

I suspect the real problem here is that callable functions aren't set up to return date objects. If I go one more step and convert the timestamp into a string, I finally get something back in the client:

return projectRef.get().then(snapshot => {
  return {
    exVersion,
    exId,
    projects: _.chain(snapshot.docs)
      .keyBy('id')
      .mapValues(s => s.data())
      .forEach(d => { d.created = d.created.toDate().toISOString() })
      .value()
  }
})

gives:

RESPONSE RECEIVED FROM FUNCTION: 200, {
  "result": {
    "exVersion": 9,
    "exId": null,
    "projects": {
      "XcRyQyaxLguRdbNmxQdn": {
        "name": "c",
        "description": "c",
        "created": "2018-11-28T11:46:27.169Z"
      }
    }
  }
}

I couldn't find anything in the documentation for callable functions about special handling for dates, but am I missing something in how this should be done?


Some further analysis in the callable function suggests JSON.stringify() is involved, but isn't the whole problem:

console.log(JSON.stringify(d.created)):
info: {"_seconds":1543405587,"_nanoseconds":169000000}
JSON.stringify(d.created.toDate())
into: "2018-11-28T11:46:27.169Z"

So calling d.created.toDate() should be sufficient. But I've had other cases where Date objects are just not returned by callable functions, e.g.:

const testObject = {
  some: 'thing',
  d: new Date()
}

console.log('JSON:', JSON.stringify(testObject))

  return projectRef.get().then(snapshot => {
    return {
      testObject,
      projects: _.chain(snapshot.docs)
        .keyBy('id')
        .mapValues(s => s.data())
        .forEach(d => { console.log('d.created is:', JSON.stringify(d.created.toDate())); d.created = d.created.toDate() })
        .value()
    }
  })

Note in the output below, the logged results of JSON.stringify() on date objects seems to work, but when those objects are contained within the object returned from the function, they both come out as empty objects:

firebase > getAllProjects({uid: 1234})
Sent request to function.
firebase > info: User function triggered, starting execution
info: JSON: {"some":"thing","d":"2018-11-28T13:22:36.841Z"}
info: Got 1 projects
info: d.created is: "2018-11-28T11:46:27.169Z"
info: Execution took 927 ms, user function completed successfully

RESPONSE RECEIVED FROM FUNCTION: 200, {
  "result": {
    "testObject": {
      "some": "thing",
      "d": {}
    },
    "projects": {
      "XcRyQyaxLguRdbNmxQdn": {
        "description": "c",
        "created": {},
        "name": "c"
      }
    }
  }
}
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
dsl101
  • 1,715
  • 16
  • 36
  • What are you expecting a serialized Date object to look like? If you want it in some format, you should be explicit about that format, not just assume some serialization. – Doug Stevenson Nov 28 '18 at 13:43
  • 1
    Something like `JSON.stringify(new Date())` or `new Date().toJSON()`. If I need a specific format, yes, but defaulting to nothing seems like the most surprising result. – dsl101 Nov 28 '18 at 15:08
  • This is what we call: **Missing in Action**. Unlike `ExpressJS`, it is automatically serialized to JSON. – Antonio Ooi Aug 18 '20 at 16:14
  • 1
    @DougStevenson : If Firebase really need to default to something, at least default to `toISOString()` rather than `{}` -- since the Firebase team still need to default it to something if not explicitly formatted anyway. `{ }` is rather misleading, which may lead to whole day debugging. – Antonio Ooi Aug 18 '20 at 16:24
  • @AntonioOoi If you have feedback or suggestions for Firebase, I suggest filing them with Firebase support. https://support.google.com/firebase/contact/support – Doug Stevenson Aug 18 '20 at 16:35

1 Answers1

7

From the documentation:

To send data back to the client, return data that can be JSON encoded.

Since Date is not a JSON type, you will have to use your own serialization format for it (at least if you want it to be reliable/portable). From the linked answer it sounds like Date.toJSON is a great candidate for that.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • So returning the contents of `document.data()`, if that document contains a `firebase.firestore.FieldValue.serverTimestamp()` just returns the 'internal' representation. And converting (as I have above) is the best way to get a consistent date string? It would seem to be such a common use case that the callable function wrapper logic could include some checks on that, or at least specifically mention it in the documentation. Same for `Date` objects within a document—seems like something very common to do. – dsl101 Nov 28 '18 at 15:07
  • Also, I'm curious how the callable functions actually do the conversion to JSON. I'm assuming it's _not_ via `JSON.stringify()`, otherwise the dates would come through as strings... – dsl101 Nov 28 '18 at 15:12
  • If you think the case could be handled better, I recommend [filing a bug report or feature request](https://firebase.google.com/support/contact/bugs-features/). To see how the conversion is actually done, have a look at the repo: https://github.com/firebase/firebase-functions – Frank van Puffelen Nov 28 '18 at 15:20
  • Looking at https.ts, it would be fairly easy to add `Date` support via `if _.isDate(data) { return data.toJSON() }` around line 365. I will file via the form, but also suggest the docs could be clarified. Since `Date.toJSON()` is a thing, saying "return data that can be JSON encoded" implies to me dates would be OK. – dsl101 Nov 28 '18 at 15:36
  • In fact, it would have to go at line 359, since `_.isObject(new Date())` returns `true`, but the subsequent call to `_.mapValues()` results in an empty object, so at least I know now where that empty object is coming from. – dsl101 Nov 28 '18 at 15:52