liontariai / samarium

https://liontari.ai/
Apache License 2.0
14 stars 0 forks source link

Lazify Operations with magic $lazy property #10

Closed liontariai closed 4 months ago

liontariai commented 4 months ago

This PR implements the Lazy Queries and Lazy Mutations features.

Use cases / requirements

Using the generated client to execute queries and mutations is nice and doing so always in direct manner may be useful and sufficient in many cases but GraphQL - especially in a frontend context - thrives by defining queries and mutations once and reusing them all over your application.

In Samarium's context this would mean that the defined operations need to defer their executions and instead of having the returned result from the server with the appropriate type instantly after await, the returned values from the client root function need to be async functions themselves which on execution will send the operation to the server. Also one would expect to be able to pass query/mutation arguments to the function.

Implementation

To make this as easy as possible for the user, a magic $lazy property is added to the Query/Mutation/Subscription Operation Type fields. This way you can define your operation as usual and in the end decide whether it should be a lazy operation or not.

Full example:

import unions from "./unions6";

const { all, books } = await unions((op) =>
    op.query((s) => ({
        all: s.search({ title: "default title" })(({ $on }) => ({
            ...$on.Book((s) => ({
                ...s.$scalars(),
            })),
            ...$on.Article((s) => ({
                ...s.$scalars(),
                dice: s.rollDice({ numDice: 3, numSides: 6 }),
            })),
        })).$lazy, // <--- return the $lazy prop
        books: s.books((s) => ({
            title: s.title,
        })).$lazy, // <--- return the $lazy prop
    })),
);

const defaultSearches = await all({});
console.log(defaultSearches.map((a) => a.title));

const defaultBooks = await books();
console.log(defaultBooks.map((a) => a.title));

const testSearches = await all({ title: "test" });
console.log(testSearches.map((a) => a.title));

const test2Searches = await all({ title: "221234" });
console.log(test2Searches.map((a) => a.title));

Explanation

Each field from the Query root type has the $lazy property which is accessed after defining your selection. You can pass arguments to your selection and these will serve as default arguments later on.

Now, instead of being executed and sent to the server, an async function is returned for every lazy operation.

These functions can now be called and awaited later in the code and arguments passed to it will be merged with the given default arguments in the original definition of the operation.

That's it. Your operations are now async typescript functions you can use anywhere in your code! 🎉

You can also mix instant and lazy operations!