Is there any example that implements Fauna.js with TS in context of fetching and creating Documents?

Current Question

What is the best practice for dealing with the Fauna fields coll, id, ttl, and ts on the client side with Fauna-js?

Problem

If I’m fetching data from Fauna, I can use DocumentT<CollectionName>, but if I need to create a new Document with a custom id or ttl, I can’t use DocumentT<CollectionName> because it also enforces coll and ts leading to a reserved field error during document creation.
Theoretically, I could now create a custom version of the Document class where coll and ts are not enforced and id is optional. However, I would need to deal with two different types for creating and fetching documents.

So, before doing such a thing, I’m curious if you already have any best practices.

Context

Fauna class Document

export declare class Document extends DocumentReference {
    readonly ts: TimeStub;
    constructor(obj: {
        coll: Module | string;
        id: string;
        ts: TimeStub;
        [key: string]: any;
    });
    toObject(): {
        coll: Module;
        id: string;
        ts: TimeStub;
    };
}

Fauna type DocumentT

export type DocumentT<T extends QueryValueObject> = Document & T;

Custom type User

type User = {
    name: string;
    email: string;
};

How are you creating new documents? This should work:

const response = client.query<Document<User>>(fql`
  User.create({
    id: "1234",
    ttl: Time.now().add(1, "day"),
    name: "Paul",
    email: "paul@me.com",
  })
`)

I made comment in your other question that may be important here:

Also, if you want to be able to read the ttl field, then you should add that to your type

type User = {
    name: string;
    email: string;
    ttl: TimeStub;
};

Thanks a lot for your Response! :slight_smile:

In your example, you provide untyped document data to the query function. I’m trying to provide typed data:

const newDocument: Document = new Document({
    coll: 'User',
    id: '1',
    ts: TimeStub.fromDate(new Date())
});

const newUser: DocumentT<User> = Object.assign(newDocument, {
    email: 'leroy.jenkins@epic.com'
});

const response = client.query<Document<User>>(fql`
  User.create(${newUser})
`)

And as written above, using DocumentT<User> enforces coll and ts, leading to a reserved field error during document creation. Theoretically, I could now create a custom version of the Document class where coll and ts are not enforced and id is optional. However, I would need to deal with two different types for creating and fetching documents. So, I’m seeking some existing best practices. :slightly_smiling_face:

My overall goal is to achieve a full typed handover from Fauna into the Client store and vice versa.

  1. Fetching typed documents from Fauna and putting them into the store. (Works with DocumentT<CollectionName>)
  2. Creating typed documents at the Client, putting them into the store, and creating them in Fauna. (Not working with DocumenT<CollectionName>, but could work with a 2nd variant of Document class where all fields are optional)

Initially, I tried a simplified version of the above code snippet, but it’s throwing a TS error:

const newUser: DocumentT<User> = new Document({
    coll: 'User',
    id: '1',
    ts: TimeStub.fromDate(new Date()),
    email: 'leroy.jenkins@epic.com'
});
Type 'Document' is not assignable to type 'DocumentT<User>'.
  Property 'email' is missing in type 'Document' but required in type 'User'.

More about the Javascript Document class

The data you provide to create a Document is fundamentally different from the Document itself.

Are you familiar with GraphQL? There is a parallel here with how there is a distinct difference between type and input. Don’t want to get to stuck on introducing more concepts, though…

The JS Document class is not meant to represent objects that only exist in user-land. That is, you should not create a Document class instance and then try to use that to create itself in FQL. Creating an instance directly with ts: TimeStub.fromDate(new Date()) is like asserting that it already exists in the database.

You would not, for example provide a Document when creating another document, but it is very similar to what you are trying to do.

// invalid FQL
let thing1 = Thing.byId('1')
Thing.create(thing1)

You would instead create a new input from an existing Document

// invalid FQL
let thing1 = Thing.byId('1')
Thing.create(thing1.data) // 'data' will not include ts, coll, or ttl

The driver is not attempting to provide you with an ORM implementation for your types. The primary reason for the provided types is to ensure values received from the database are sent back in the same shape. In the case of Documents, they are wrapped in an object in a @doc field. By sending back to fauna with the same encoding, you can do things like call methods on the document, or anything else you would do with a document:

${jsDocument}.update({...})

${jsDocument}.delete({...})

Thing.create({
  relationship: ${jsDocument}
})

Thing.byRelationship(${jsDocument})

Note that the JS Document class fields are marked readonly. It might also be a good idea, now that I think about it, to wrap the whole type in the TS utility ReadOnly<Document & T>.

Workaround

focus on your own domain type

I would recommend creating an instance of your own Domain instance, here User, then create it with an id if necessary.

const newUser: User = {
    email: 'leroy.jenkins@epic.com'
}

const response = client.query<Document<User>>(fql`
  User.create(${{
    id: '1'
    ...newUser
  })
`)

I think it should also be okay to include the id in your User type

type User = {
    id: string;
    email: string;
    name: string;
    ttl?: TimeStub;
};
const newUser: User = {
    id: '1',
    email: 'leroy.jenkins@epic.com'
}

const response = client.query<Document<User>>(fql`
  User.create(${newUser})
`)

Or you can add an additional type to your app which is something like UserInput

type UserInput = {
    email: string;
    name: string;
    id?: string;
    ttl?: TimeStub;
};

use the data field on documents

const newDocument: Document = new Document({
    coll: 'User',
    id: '1',
    ts: TimeStub.fromDate(new Date())
});

const newUser: DocumentT<User> = Object.assign(newDocument, {
    email: 'leroy.jenkins@epic.com'
});

const response = client.query<Document<User>>(fql`
  User.create(${newUser}.data)
`)

note how .data is called in FQL, not JS.

use delete or assign fields to undefined

This could give you more flexibility to define whatever input you want

const response = client.query<Document<User>>(fql`
  User.create(${{ ...newUser, coll: undefined, ts: undefined }})
`)
1 Like

Alright, thanks a lot for your inspiration!
Documenting here my final solution:

I’ve now created a type ClientDocument as a counterpart to Document:

type ClientDocument = {
  coll?: string;
  id?: string;
  ts?: TimeStub;
  ttl?: TimeStub;
};

And my Collection types are extending it:
e.g. User

type User = ClientDocument & {
  name: string;
  email: string;
}

Fetch document:

const user: User = (await client.query<DocumentT<User>>(query)).data;

Create a document

const user: User = {
  name: "Leroy",
  email: "leroy@jenkins.com"
}
await client.query<DocumentT<User>>(fql`User.create(${user})`)
1 Like

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.