Skip to content

royalcala/urql-tinybase-exchange

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

urql-tinybase-exchange

A Urql exchange for integrating with TinyBase, allowing automatic persistence and synchronization of GraphQL data into a local reactive store.

Features

  • Automatic Persistence: Use the @dbMergeRow directive to automatically save query and mutation results to TinyBase.
  • Automatic Deletion: Use the @dbDeleteRow directive to remove rows from TinyBase.
  • Reactive Queries: Seamlessly integrates with TinyBase's reactive components (useCell, useRow, useQuery).
  • Advanced Support: Fully compatible with TinyBase Indexes, Metrics, and Queries.
  • Works with Queries and Mutations: Sync data from both GraphQL queries and mutations.

Installation

npm install urql-tinybase-exchange urql tinybase graphql react

Usage

1. Setup the Client

import { createClient, fetchExchange } from "urql";
import { createStore } from "tinybase";
import { tinyBaseExchange } from "urql-tinybase-exchange";

const store = createStore();

const client = createClient({
  url: "http://localhost:4000/graphql",
  exchanges: [tinyBaseExchange({ store }), fetchExchange],
});

2. Use Directives in Queries and Mutations

The table argument can be either a string literal or an enum value:

# Define enum in your GraphQL schema (optional but recommended)
enum Table {
  Post
  Comment
  User
  Reaction
}

directive @dbMergeRow(table: Table!) on FIELD | FRAGMENT_DEFINITION
directive @dbDeleteRow(table: Table!) on FIELD

# Or use string literals
directive @dbMergeRow(table: String!) on FIELD | FRAGMENT_DEFINITION
directive @dbDeleteRow(table: String!) on FIELD

Using with Queries (Enum or String):

# Using enum (Post will be converted to lowercase "post" for TinyBase table)
fragment PostFragment on Post @dbMergeRow(table: Post) {
  id
  title
  author @dbMergeRow(table: User) {
    id
    name
  }
}

# Or using string literal
fragment PostFragment on Post @dbMergeRow(table: "posts") {
  id
  title
  author @dbMergeRow(table: "users") {
    id
    name
  }
}

query GetPosts {
  posts {
    ...PostFragment
  }
}

This will automatically sync all posts and their authors to TinyBase when the query returns.

Nested Objects and Primitives

TinyBase table cells store primitive values only (string, number, boolean, null). Nested objects and arrays from your GraphQL response are not retained on the parent row. To persist nested structures, apply @dbMergeRow on the nested object/array fields so they are normalized into their own tables.

  • Enum table names are converted to lowercase: Postpost. If you prefer pluralized or custom names, use string literals like "posts".
  • Deletion uses field-level directives on the ID(s): apply @dbDeleteRow directly to the field that returns the ID or array of IDs.
  • Client-only directives are stripped before sending to the backend, so servers won’t see @dbMergeRow/@dbDeleteRow.

Example with nested fragments and arrays:

fragment UserFragment on User {
  id
  name
}
fragment ReactionFragment on Reaction {
  id
  emoji
  user @dbMergeRow(table: "users") {
    ...UserFragment
  }
}
fragment CommentFragment on Comment {
  id
  text
  author @dbMergeRow(table: "users") {
    ...UserFragment
  }
  reactions @dbMergeRow(table: "reactions") {
    ...ReactionFragment
  }
}

# Using an enum for the parent table; will map to lowercase "post"
fragment PostFragment on Post @dbMergeRow(table: Post) {
  id
  title
  comments @dbMergeRow(table: "comments") {
    ...CommentFragment
    # Nested array of replies, also merged into the "comments" table
    replies @dbMergeRow(table: "comments") {
      ...CommentFragment
    }
  }
}

mutation CreatePostWithNested {
  createPost(id: "p1", title: "Nested") {
    ...PostFragment
  }
}

In this example:

  • The post is stored in the post table (lowercased from enum). Primitive fields like id, title, and potentially __typename are stored on the row.
  • comments, replies, author, and reactions are stored in their own tables via nested @dbMergeRow directives.
  • If you need to keep a JSON blob in a single cell, serialize it (e.g., contentJson as a string), understanding you won’t be able to index/query inside that blob via TinyBase.

Merging Data (@dbMergeRow):

mutation CreateUser {
  createUser(id: "1", name: "Alice") @dbMergeRow(table: "users") {
    id
    name
  }
}

This will automatically do store.setRow('users', '1', { id: '1', name: 'Alice' }).

Deleting Data (@dbDeleteRow):

mutation DeleteUser {
  deleteUser(id: "1") {
    id @dbDeleteRow(table: "users")
  }
}

This will automatically do store.delRow('users', '1'). The directive is applied to the id field which contains the row ID to delete.

You can also delete multiple rows by applying the directive to an array field of IDs:

mutation DeleteUsers {
  deleteUsers(ids: ["1", "2"]) {
    ids @dbDeleteRow(table: "users")
  }
}

3. React Integration

To use TinyBase hooks like useCell, useRow, or useQuery, you must wrap your app with the Provider from tinybase/ui-react and pass the same store instance you used for the exchange.

import React from "react";
import { createClient, Provider as UrqlProvider, fetchExchange } from "urql";
import { createStore } from "tinybase";
import { Provider as TinyBaseProvider, useCell } from "tinybase/ui-react";
import { tinyBaseExchange } from "urql-tinybase-exchange";

// 1. Create the store
const store = createStore();

// 2. Create the client with the exchange using the SAME store
const client = createClient({
  url: "http://localhost:4000/graphql",
  exchanges: [tinyBaseExchange({ store }), fetchExchange],
});

const UserProfile = ({ id }) => {
  // 4. Use standard TinyBase hooks
  const name = useCell("users", id, "name");
  return <div>User: {name}</div>;
};

export const App = () => (
  // 3. Wrap your app with BOTH providers
  <TinyBaseProvider store={store}>
    <UrqlProvider value={client}>
      <UserProfile id="1" />
    </UrqlProvider>
  </TinyBaseProvider>
);

Since TinyBase is reactive, standard hooks will automatically trigger updates when the exchange modifies the store:

import { useCell } from "tinybase/ui-react";

const UserParams = ({ id }) => {
  const name = useCell("users", id, "name");
  return <div>User: {name}</div>;
};

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages