Github|...

Query Data

Querying data in Spooky is designed to be intuitive and reactive. The system handles the entire lifecycle of a query, from initialization to registration and liveness updates, ensuring your UI is always consistent with the database state.

Query Lifecycle

  1. Initialization: When a component requests data, Spooky checks if a live query for that data already exists.
  2. Registration: If not, it registers a new query with the sync engine.
  3. Liveness: The query remains “live” as long as there are active subscribers. Spooky automatically manages subscriptions and unsubscription to optimize performance.

Fluent Query API

The db.query() method provides a fluent interface to construct queries. It is fully type-safe based on your schema.

TypeScript
// Build a query - returns a query object, not the results
const query = db.query('user')
.where({ age: { $gt: 18 } })
.orderBy('created_at', 'desc')
.limit(10)
.build();

// Execute with useQuery hook (reactive)
const usersQuery = useQuery(db, () => query);
const users = usersQuery.data();

Relationships

Spooky makes fetching related data incredibly simple with the .related() method. It automatically handles the underlying graph traversals or joins.

Unified Query Syntax

Whether you are fetching a simple 1:1 link, a 1:N collection, or traversing a complex N:M graph, the syntax remains exactly the same. Spooky uses your schema definition to determine how to fetch the data.

TypeScript
// Fetch threads with their authors (1:1)
const threadsQuery = db.query('thread')
.related('author')
.orderBy('created_at', 'desc')
.limit(10)
.build();

// Use in a component with useQuery
const threads = useQuery(db, () => threadsQuery);

// Access related data
threads.data()?.forEach(thread => {
console.log(thread.title);
console.log(thread.author?.username); // Automatically typed!
});
Note

Magic behind the scenes: You don’t need to manually specify graph paths (e.g., ->liked->post) or join conditions. Spooky inspects your schema.surql to understand that liked is a relation table connecting users to posts, and generates the correct query automatically.

Type Safety

One of Spooky’s strongest features is its end-to-end type safety.

When you use .related(), the return type of your query is automatically adjusted.

  • Without .related(): The field is a string (Record ID).
  • With .related(): The field becomes the full related object with selected fields.
TypeScript
const post = posts[0];

// ✅ TypeScript knows this is available
console.log(post.author.username);

// ❌ Error: Property 'email' does not exist on type 'User' (we only selected username/avatar)
console.log(post.author.email);

This ensures you can never accidentally access a property on a relation that hasn’t been fetched. TypeScript will catch the error at compile time.

Using Hooks (SolidJS Example)

For reactive applications, Spooky provides hooks that automatically update your component when the data changes.

TypeScript
import { useQuery } from '@spooky-sync/client-solid';
import { For } from 'solid-js';
import { db } from './db';

function UserList() {
// Build query
const query = db.query('user')
  .orderBy('created_at', 'desc')
  .limit(20)
  .build();

// Hook automatically subscribes to updates
const usersResult = useQuery(db, () => query);

return (
  <ul>
    <For each={usersResult.data() || []}>
      {(user) => <li>{user.username}</li>}
    </For>
  </ul>
);
}