@dvina/sdk
v3.3.124
Published
Type-safe SDK for the Dvina GraphQL API
Downloads
64
Readme
@dvina/sdk
Type-safe SDK for the Dvina GraphQL API. Offline-first architecture with normalized local store (IndexedDB), real-time sync, and framework adapters for Angular and React.
Installation
npm i @dvina/sdkWorkspace Development
From the repository root:
pnpm bootstrap
pnpm --filter @dvina/sdk run generate:workspace
pnpm --filter @dvina/sdk run buildOr use the root shortcut:
pnpm build:sdkgenerate:workspace first builds the codegen helper packages, then regenerates SDK sources. build:workspace runs the full generate + build flow in one command.
Peer dependency:
npm i graphqlFramework adapters (optional):
# Angular
npm i @angular/core
# React
npm i reactQuick Start
import { DvinaSdk } from '@dvina/sdk';
const sdk = new DvinaSdk({ token: 'your-auth-token' });For production apps with token refresh:
const sdk = new DvinaSdk({
getToken: async () => {
// return a fresh token from your auth provider
return await authService.getAccessToken();
},
});Options
| Option | Type | Description |
| ---------- | -------------------------- | ------------------------------------------------------ |
| token | string | Static auth token (scripts, CLI, tests) |
| getToken | () => Promise<string> | Async token callback (production apps) |
| baseUrl | string | API base URL without protocol. Default: api.dvina.ai |
| language | string \| (() => string) | Preferred language for API responses |
Queries
Fetching a Single Entity
const chat = await sdk.chat({ id: 'chat-id' }).fetch();
console.log(chat.id);
console.log(chat.title);Fetching a Connection (List)
const reports = await sdk.reports({ first: 20 }).fetch();
for (const report of reports.nodes) {
console.log(report.id, report.name);
}
console.log(reports.totalCount);
console.log(reports.pageInfo.hasNextPage);Smart Fetch (Including Relations)
Chain relation methods before .fetch() to include related data in a single request:
// Include agent data inline with each chat
const chats = await sdk.chats({ first: 50 }).agent().fetch();
// agent is directly available on each node (no extra request)
for (const chat of chats.nodes) {
console.log(chat.agent.id, chat.agent.name);
}
// Single entity with includes
const chat = await sdk.chat({ id: 'chat-id' }).agent().fetch();
console.log(chat.agent.name);Nested includes with variables:
const reports = await sdk.reports({ first: 100 }).insights({ first: 10 }).fetch();
for (const report of reports.nodes) {
for (const insight of report.insights.nodes) {
console.log(insight.title);
}
}Lazy Relation Fetching
Without Smart Fetch, relations are fetched on demand via method calls:
const chat = await sdk.chat({ id: 'chat-id' }).fetch();
// Separate request for the agent
const agent = await chat.agent().fetch();
console.log(agent.name);Pagination
Connections follow the Relay cursor specification with built-in pagination helpers:
const reports = await sdk.reports({ first: 20 }).fetch();
// Load next page (appends nodes, updates pageInfo)
if (reports.pageInfo.hasNextPage) {
await reports.fetchNext();
}
// Load previous page (prepends nodes, updates pageInfo)
if (reports.pageInfo.hasPreviousPage) {
await reports.fetchPrevious();
}Utility functions:
import { extractNodes, mergeConnections } from '@dvina/sdk/pagination';
const allNodes = extractNodes(reports);
// Returns a new connection without modifying the originals
const merged = mergeConnections(firstPage, secondPage);Mutations
// Create
const agent = await sdk.createAgent({
data: { name: 'My Agent', description: 'An assistant' },
});
// Update
const updated = await sdk.updateReport({
where: { id: 'report-id' },
data: { name: 'Updated Name' },
});
// Delete
await sdk.deleteChat({ where: { id: 'chat-id' } });Mutations automatically update the local store with optimistic updates and cache rules.
Reactive Queries (watch)
Use .watch() instead of .fetch() to get a reactive DvinaQueryRef that emits new values whenever the underlying data changes (via local mutations, delta sync, or cache updates):
const ref = sdk.reports({ first: 20 }).watch();
// Vanilla JavaScript — AsyncIterable
for await (const reports of ref) {
console.log('Updated:', reports.nodes.length);
}
// Clean up when done
ref.dispose();DvinaQueryRef properties:
| Property/Method | Description |
| --------------- | ------------------------------------------ |
| current | Most recently emitted value (or undefined) |
| refetch() | Force re-fetch from network |
| fetchMore() | Fetch additional pages and merge |
| dispose() | Unsubscribe and clean up |
Subscriptions
Real-time data via WebSocket (graphql-ws protocol):
const stream = sdk.sendMessageStream({
chatId: 'chat-id',
content: 'Hello!',
});
for await (const event of stream) {
console.log(event);
}File Upload
const file = new File(['content'], 'document.pdf', { type: 'application/pdf' });
const result = await sdk.uploadFile(file, 'document.pdf', {
onProgress: (event) => {
console.log(`${event.percent}% uploaded`);
},
});
console.log('File ID:', result.id);Framework Adapters
Angular
import { toSignal } from '@dvina/sdk/angular';
@Component({
selector: 'app-reports',
template: `
@for (report of reports()?.nodes; track report.id) {
<div>{{ report.name }}</div>
}
`,
})
class ReportsComponent {
private sdk = inject(DvinaSdk);
reports = toSignal(this.sdk.reports({ first: 20 }).watch());
}toSignal automatically disposes the query ref when the component is destroyed via Angular's DestroyRef.
For usage outside an injection context:
const destroyRef = inject(DestroyRef);
// Later, in a method:
this.reports = toSignal(ref, { destroyRef });dvnResource
A resource primitive inspired by Angular's resource() API. It wraps both Promise (.fetch()) and AsyncIterable (.watch()) loaders into a signal-based interface with status and error tracking.
import { dvnResource } from '@dvina/sdk/angular';With reactive params — one-shot fetch (Promise):
@Component({ ... })
class UserComponent {
private sdk = inject(DvinaSdk);
userId = input.required<string>();
userResource = dvnResource({
params: () => ({ id: this.userId() }),
loader: ({ params }) => this.sdk.user(params).fetch(),
});
// template:
// @if (userResource.isLoading()) { <spinner /> }
// @if (userResource.value(); as user) { <h1>{{ user.name }}</h1> }
// @if (userResource.error(); as err) { <p>{{ err.message }}</p> }
}With reactive params — live query (AsyncIterable):
@Component({ ... })
class ReportsComponent {
private sdk = inject(DvinaSdk);
reportsResource = dvnResource({
params: () => ({ first: 20 }),
loader: ({ params }) => this.sdk.reports(params).watch(),
});
// Value updates automatically whenever the underlying store changes
}Without params — runs immediately:
configResource = dvnResource({
loader: () => this.sdk.config().fetch(),
});The loader return type is detected at runtime:
- Returns a
Promise→ resolved once, status becomes'resolved' - Returns an
AsyncIterable/DvinaQueryRef→ consumed as a live stream, value updates on every emission
When params() returns undefined, the resource enters 'idle' status and the loader does not run. This is useful for conditional fetching:
reportResource = dvnResource({
params: () => {
const id = this.reportId();
return id ? { id } : undefined; // undefined → idle, no request
},
loader: ({ params }) => this.sdk.report(params).fetch(),
});DvnResourceRef API:
| Property/Method | Type | Description |
| --------------- | ---------------------------- | --------------------------------------------------------------------- |
| value | Signal<T \| undefined> | Most recently resolved value |
| status | Signal<DvnResourceStatus> | 'idle' | 'loading' | 'reloading' | 'resolved' | 'error' |
| isLoading | Signal<boolean> | true when 'loading' or 'reloading' |
| error | Signal<Error \| undefined> | Last error thrown by the loader |
| hasValue() | boolean | Whether value() is not undefined |
| reload() | void | Re-run the loader with current params |
| destroy() | void | Abort/dispose active loader, reset to 'idle' |
Cleanup is automatic via DestroyRef — the loader is aborted (Promise) or disposed (AsyncIterable) when the component is destroyed.
React
import { useLiveQuery } from '@dvina/sdk/react';
function ReportsPage() {
const reports = useLiveQuery(() => sdk.reports({ first: 20 }).watch(), []);
if (!reports) return <div>Loading...</div>;
return (
<ul>
{reports.nodes.map((r) => (
<li key={r.id}>{r.name}</li>
))}
</ul>
);
}The factory function is memoized internally — the query ref is only created when the dependency array changes. This prevents infinite dispose-subscribe loops that would occur with inline .watch() calls.
// Reactive query — ref recreated when `id` changes
function ReportDetail({ id }: { id: string }) {
const report = useLiveQuery(() => sdk.report({ id }).watch(), [id]);
if (!report) return <div>Loading...</div>;
return <h1>{report.name}</h1>;
}useLiveQuery automatically disposes the query ref when the component unmounts or when dependencies change.
Error Handling
The SDK provides a typed error hierarchy:
import { DvinaError, DvinaGraphQLError, DvinaNetworkError, DvinaAuthenticationError } from '@dvina/sdk';
try {
await sdk.chat({ id: 'invalid' }).fetch();
} catch (error) {
if (error instanceof DvinaAuthenticationError) {
// 401/403 — redirect to login
} else if (error instanceof DvinaGraphQLError) {
// Server returned GraphQL errors
console.error(error.errors);
} else if (error instanceof DvinaNetworkError) {
// Connection failure, timeout, etc.
console.error(error.status);
}
}| Error Class | When |
| -------------------------- | ------------------------------------- |
| DvinaError | Base class for all SDK errors |
| DvinaGraphQLError | GraphQL response contains errors |
| DvinaNetworkError | Network failure, timeout, non-OK HTTP |
| DvinaAuthenticationError | 401/403 authentication failure |
Architecture
The SDK uses an offline-first, normalized store architecture:
- Queries are routed through the
SyncEngine, which normalizes GraphQL responses into entity tables in IndexedDB (via Dexie). - Mutations apply optimistic updates immediately, then reconcile with server responses.
- Delta Sync maintains a persistent SSE connection that streams entity changes, keeping the local store up to date in real-time.
- Reactive queries (
watch()) are backed by Dexie'sliveQuery— any write to a relevant table automatically re-emits.
DvinaSdk
├── Queries/Mutations → HTTP Transport → SyncEngine → IndexedDB (Dexie)
├── Subscriptions → WebSocket Transport (graphql-ws)
└── Delta Sync → SSE Transport → IndexedDB (Dexie)Cleanup
// Stop delta sync
sdk.stopSync();
// Full teardown (stops sync, terminates WebSocket)
sdk.destroy();License
MIT
