Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add out-of-the-box date support #4

Closed
nandorojo opened this issue May 11, 2020 · 5 comments
Closed

Add out-of-the-box date support #4

nandorojo opened this issue May 11, 2020 · 5 comments

Comments

@nandorojo
Copy link
Owner

  1. Add support for .toDate() by recursively checking an object for any server dates and converting them into Date objects.
  2. By default, enable the serverTimestamp field (I forget what the field called, this relates to onSnapshot subscriptions creating pseudo timestamps before the local stuff hits the backend.)
@nandorojo
Copy link
Owner Author

nandorojo commented Jun 6, 2020

Proposed Solution

Pass a parseDates option to useDocument or useCollection.

  • parseDates could be an array of dot-notation strings used by lodash to turn firestore dates into JS Date objects.
  • It could also be a function that takes in the document and returns a parsed firestore date.

Use Case

You have a document with this kind of format:

// users/one
{
  createdAt: Date
}

If you do this:

const { data } = useDocument('users/one')

let createdAt: Date
if (data) {
  // 🚨 error! must turn firestore date into JS date object
  createdAt = data.createdAt
}

You get an error. We must run data.createdAt.toDate(), which turns a firestore date into a date object like this:

const { data } = useDocument('users/one')

let createdAt: Date
if (data) {
  // ✅ works!
  createdAt = data.createdAt.toDate()
}

API

It would be nice if this parsing worked for you under-the-hood, using the parseDates prop.

Taking the previous example, where we have a users/one document with a createdAt date field, a useful api could work like this:

const { data } = useDocument('users/one', {
  parseDates: ['createdAt'] // nested fields would look like "user.createdAt"
})

let createdAt: Date
if (data) {
  // ✅ works!
  createdAt = data.createdAt
}

No more need for toDate()!

Function API

This same field could also accept a function (which I would memoize under the hood) that lets you parse a document itself. A bit less magical, but maybe you want more control. The api would look like this:

const { data } = useDocument('users/one', {
  parseDates: (doc) => ({ ...doc, createdAt: doc.createdAt.toDate() })
})

let createdAt: Date
if (data) {
  // ✅ works!
  createdAt = data.createdAt
}

Roadblocks

The roadblocks to the current implementation are mostly TypeScript suggestions.

Since we give useDocument a generic with our TypeScript schema, how do we tell it that we want to turn dates into others?

Here is how I currently work around this in my apps:

import * as firebase from 'firebase'

// If S = true, it's a server date. If S = false, it's a JS Date
type ServerDate<S extends boolean = false> = S extends true ? firebase.firestore.Timestamp : Date

// Create document TS schema
type Doc<IsServer extends boolean = false> = {
  createdAt: ServerDate<IsServer>
}

type ServerDoc = Doc<true>
type ParsedDoc = Doc // implicit false

const useUser = () => {
  const { data, ...rest } = useDocument<ServerDoc>('users/one')

  // function that takes in a doc and returns the parsed doc
  const parseForRender = (doc: ServerDoc): ParsedDoc => {
    return {
      ...doc,
      createdAt: doc.createdAt.toDate()
    }
  }

  return { data: data && parseForRender(data), ...rest }
}

I think a good solution is just to have the parseDates function take an unknown argument, which you explicitly declare, but it must return the correct document schema.

Example:

import * as firebase from 'firebase'

// If S = true, it's a server date. If S = false, it's a JS Date
type ServerDate<S extends boolean = false> = S extends true ? firebase.firestore.Timestamp : Date

// Create document TS schema
type Doc<IsServer extends boolean = false> = {
  createdAt: ServerDate<IsServer>
}

type ServerDoc = Doc<true>
type ParsedDoc = Doc // implicit false

// pass the normal *parsed doc* field here
const { data } = useDocument<ParsedDoc>('users/one', {
  // we *explicitly* tell TS that the argument is a ServerDoc
  // useDocument will enforce that it returns a ParsedDoc type
  parseDates: (doc: ServerDoc) => ({ ...doc, createdAt: doc.createdAt.toDate() })
})

The only downside to the above case is that I might need to @ts-ignore a little bit in the library, but it's not the end of the world.

Code

The lib could have a documentDateParser function.

import { get, set } from 'lodash'

type ParseDatesOption<Data> = string[] | ((doc: unknown) => Data)

function dateParser<Data>(doc: Data, parseDates: ParseDatesOptions<Data>) {
  let finalDoc = { ...doc }

  if (Array.isArray(parseDates)) {
    parseDates.forEach((dateField) => {
      const unparsedDate = get(dateField)
      if (unparsedDate) {
        const parsedDate = unparsedDate.toDate && unparsedDate.toDate()
        if (parsedDate) {
          set(finalDoc, dateField, parsedDate)
        }
      }
    })
  } else if (typeof parseDates === 'function') {
    finalDoc = parseDates(doc)
  }

  return finalDoc
}

@nandorojo nandorojo changed the title Fix date support Add out-of-the-box date support Jun 18, 2020
@nandorojo
Copy link
Owner Author

Added in 0.8.0.

API

type Data = {
  createdAt: Date
  lastUpdated: {
    date: Date
  }
}


const { data } = useDocument<Data>('users/fernando', {
  parseDates: ['createdAt', 'lastUpdated.date']
})

let createdAt: Date
let updatedAt: Date
if (data) {
  createdAt = data.createdAt // ✅ JS date!
  updatedAt = data.lastUpdated.date // ✅ JS date!
}

I went with the array of object key strings as the API. No function.

@nandorojo
Copy link
Owner Author

Will close once I add an example to the readme.

@alejandrosofter
Copy link

hi how are you! .. i can t compare dates in where field ..setWhere([["fecha", ">", data.fechaDesde]
, ["fecha", "<", data.fechaHasta]]) .. can you help me please.. i test with a timestamp and not work

@alejandrosofter
Copy link

setWhere([["fecha", ">", data.fechaDesde] , ["fecha", "<", data.fechaHasta]])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants