Allow us to return graphql data back to the user from a given timestamp of our choosing instead of always using 'now'

I was trying to cascade-delete a Room and its associated RevenueSubitems in GraphQL today, and realized that it’s near-impossible to return the deleted Room document as it was just before the delete.

I am proposing a new built-in function which works similar to At, but basically sends a query with time-metadata to fauna’s graphql resolver in order to return data back to the user at a given time, rather than the current time.

The setup:

type Room {
  name: String!
  revenue: [RevenueSubitem!]! @relation
}

type RevenueSubitem {
  note: String
  payment: Long!
  room: Room! @relation
}

type Mutation {
  deleteRoomCascade(
    id: ID!
  ): Room! @resolver
}

So you can see that I have a Room type which might have several RevenueSubitem. And when I do a cascade delete of a room, I want to get it’s _id, and all of the _id of the subitems that were deleted as well:

mutation DeleteRoom($id: ID!) {
  deleteRoomCascade(id: $id) {
    _id
    revenue {
      data {
        _id
      }
    }
  }
}

So you can imagine in my resolver, I’m going to get the document for the Room that I will be deleting. Then get it’s associated subitems. Then delete those subitems. Then delete the Room. Like so:

q.Update(q.Function('deleteRoomCascade'), {
  body: q.Query(
    q.Lambda(
      ['roomId'],
      q.Let(
        {
          roomRef: q.Ref(
            q.Collection('Room'),
            q.Var('roomId')
          ),
          roomDoc: q.Get(q.Var('roomRef')),
          roomRevenueSubitemsArray: q.Paginate(
            q.Match(
              'revenueSubitem_room_by_room',
              [q.Var('roomRef')]
            ),
            { size: 1000 }
          )
        },
        q.Do(
          q.Foreach(
            q.Var('roomRevenueSubitemsArray'),
            q.Lambda(roomRevenueSubitemRef =>
              q.Delete(roomRevenueSubitemRef)
            )
          ),
          q.Delete(q.Var('roomRef')),
          q.Var('roomDoc')
        )
      )
    )
  )
})

And now I am unable to return the data that I need back to the client.

Sure, I can return the document from the deleted Room since I stored it prior to its deletion. But Fauna’s graphql engine will then automatically try to do a match on that document’s ref to find associated RevenueSubitem, and come up emptyhanded because I have already deleted them.

So I propose allowing a syntax like this to get around this issue:

q.Update(q.Function('deleteRoomCascade'), {
  body: q.Query(
    q.Lambda(
      ['roomId'],
      q.Let(
        {
          roomRef: q.Ref(
            q.Collection('Room'),
            q.Var('roomId')
          ),
          roomDoc: q.Get(q.Var('roomRef')),
          roomRevenueSubitemsArray: q.Paginate(
            q.Match(
              'revenueSubitem_room_by_room',
              [q.Var('roomRef')]
            ),
            { size: 1000 }
          ),
          preDeleteTime: q.Now()
        },
        q.Do(
          q.Foreach(
            q.Var('roomRevenueSubitemsArray'),
            q.Lambda(roomRevenueSubitemRef =>
              q.Delete(roomRevenueSubitemRef)
            )
          ),
          q.Delete(q.Var('roomRef')),
          q.GraphQLLookupSnapshot(q.Var('roomDoc'), q.Var('preDeleteTime'))
        )
      )
    )
  )
})

Two main differences. I store the time before performing the cascade delete. Then I pass that time along with my stored Room document into the proposed built-in GraphQLLookupSnapshot function. Now when this function looks for the RevenueSubitem associated with my Room, it will do so using the timestamp that I passed as the second parameter, all the data will be there, and my client will be happy since it will get all of the ids that it expects.

Hi @wallslide,

I have an idea on how to do it, and if you can share with me some sample docs (Room and RevenueSubitem) I can give a try.

Luigi

Here is a an example of creating a Room with a single RevenueSubitem referring to it:

Do(
  Create(
    Ref(Collection("Room"), "1"),
    {
      data: {
        name: "101"
      }
    }
  ),
  Create(
    Ref(Collection("RevenueSubitem"), "2"),
    {
      data: {
        payment: 10000,
        room: Ref(Collection("Room"), "1")
      }
    }
  )
)

Hi @wallslide,

might works that way?

 Update(Function('deleteMonthlyPropertyRoomCascade'), {
  body: Query(
    Lambda(
      ['roomId'],
      Let(
        {
          roomRef: Ref(Collection('Room'),Var('roomId')),
          roomDoc: Get(Var('roomRef')),
          roomRevenueSubitemsArray: Paginate(
            Match(
              'roomRevenueSubitem_room_by_room',
              [Var('roomRef')]
            ),
            { size: 1000 }
          )
        },
        {
          removedRoom: Var('roomDoc'),
          removedRevenueSubitem:
          Do(
            Map(
              Var('roomRevenueSubitemsArray'),
              Lambda('roomRevenueSubitemRef',Delete(Var('roomRevenueSubitemRef')))
            ),
            Delete(Var('roomRef')),
            Var('roomRevenueSubitemsArray')
          )
        }
      )
    )
  )
})

Luigi

@Luigi_Servini thanks for the suggestion. I see where you are going with that, but there’s no straightforward way that I know of to return a custom object that contains documents/references of types.

I guess I could create a custom @embedded type which only holds primitives and transform existing documents to such a type each time I need something like this. But this kind of goes against one of the biggest strengths of GraphQL, arbitrary querying of data based on your existing data model.

My proposal would allow for using the full expressiveness of GraphQL. And it leverages one of the key differentiators of FaunaDB - its temporality capabilities - to provide something that is both useful and difficult for other database implementations to achieve :slight_smile:

I was thinking about this some more. And I wanted to add that currently, it’s really hard to use FaunaDB’s temporality with the way the GraphQL engine works. If you need to return data based on how the state of the DB was in the past, you can only do that for a single document. All of the data return based on relations to that document would be based on the database’s current state. So having a function like GraphQLLookupSnapshot enables a bunch of new usecases in GraphQL-land.

Hi @wallslide,

you can probably use At() in your UDF (resolver) and querying the database at the given time.

Luigi

@embedded objects can contain Collection types, that are then expandable. The generated queries just don’t add any relation/connect queries to it.

I’ve had success with this schema pattern (Note that this in no way helps the temporal part of your question):

type ItemType1 { ... }
type ItemType2 { ... }

# for embedding a list of Refs
type ItemType2Wrapper @embedded {
  item: ItemType2 !
}

type InvolvedFunctionPayload @embedded {
  item1:  ItemType1 
  otherItems: [ItemType2Wrapper!]!
}

type Mutation {
  doInvolvedFunction: [InvolvedFunctionPayload!]!
    @resolver(name: "doInvolvedFunction") # or with paginated: true
}

The resolver function has to Get the Refs directly to fill out @embedded.

The wrapper type is unfortunate, but there is already a topic (somewhere) about expanding or creating a new directive for complex user types, since this is I guess not what @embedded was really intended for.

1 Like

It doesn’t seem to work, because the following of references in the document returned by my resolver is handled outside of my code on Fauna’s side, and I don’t have any way to control that behavior from my UDF. Wrapping my query in an At() in my resolver will only affect the data that I return from my UDF, not the reference-resolution that happens after that.

:exclamation: Gotcha, that makes a lot of sense.