@datanimbus/dnio-sdk
v0.1.0
Published
DNIO SDK — modular JavaScript client for the DataNimbus platform.
Downloads
122
Readme
DNIO SDK
Modular JavaScript client libraries for the DataNimbus (DNIO) platform. Designed for dashboards and apps that consume DNIO — read app/service metadata, run records CRUD, trigger lifecycle ops (deploy / publish / start / stop / sync), inspect runtime interactions.
Authoring is OUT of scope. Creating data-service definitions, designing data pipes (flows), installing plugins, and creating / editing deployment groups are intentionally not exposed. Use the MCP server (@datanimbus/dnio-mcp) or the platform UI for those. The SDK gives you everything you need to consume the platform — read state, run records CRUD, and trigger lifecycle ops on existing artifacts.
JS + JSDoc only, no TypeScript build step. Use the umbrella package for convenience, or import only the feature you need to keep your bundle slim.
Install
npm install @datanimbus/dnio-sdk
# or
yarn add @datanimbus/dnio-sdk
# or
pnpm add @datanimbus/dnio-sdkThat single install brings in the entire SDK. Node 18+ required (native fetch). Modern browsers via your bundler (Vite, Next, webpack, esbuild — all CJS-friendly).
// Whole umbrella — most apps:
const { DNIO } = require('@datanimbus/dnio-sdk');
// Cherry-pick — keep your bundle slim:
const { HttpClient } = require('@datanimbus/dnio-sdk/core');
const { AuthClient } = require('@datanimbus/dnio-sdk/auth');
const { RecordsClient } = require('@datanimbus/dnio-sdk/records');ES modules work too via the same subpath exports:
import { DNIO } from '@datanimbus/dnio-sdk';
import { RecordsClient } from '@datanimbus/dnio-sdk/records';Layout
dnio-sdk/
├── package.json
└── src/
├── index.js # umbrella — DNIO class
├── core/ # HttpClient, storage adapters, errors
├── auth/ # login, logout, session, refresh
├── apps/ # AppsClient + AppContext + AppHandle + ServiceHandle
├── services/ # data-service read + lifecycle (no authoring)
├── records/ # full Swagger surface on /api/c/{app}/{path}
├── connectors/ # instances + marketplace types
├── plugins/ # read-only catalog browse
├── flows/ # list / get / publish (no authoring)
├── deployments/ # list / get / start / stop / sync (no authoring)
├── data-formats/ # list + get
├── api-keys/ # full lifecycle
├── interactions/ # runtime observability
├── agents/ # stub — future
└── workflows/ # stub — futureUsage
Handle chain (recommended)
The handle chain mirrors how you think about the platform: app → service → action. Args are pre-bound, so the LLM (or your IDE) can dot-walk the model and never has to thread appName / servicePath through every call.
const { DNIO } = require('@datanimbus/dnio-sdk');
const dnio = new DNIO({
baseUrl: 'https://your.dnioinstance.io',
// persistCredentials: true, // silent re-login on token expiry
// storage: new CookieStorageAdapter(), // override storage backend
// onTokenExpired: (err) => routeToLogin(), // consumer-handled expiry
});
// 1. Auth
if (!(await dnio.initialize())) {
await dnio.login({ username: '[email protected]', password: '...' });
}
// 2. Bind to an app — synchronous, no network yet.
const app = dnio.app('MCP');
// 3. Bind to a service — auto-loads the app's service registry on first call.
const svc = await app.service('customerProfile');
// svc.info.serviceId // 'SRVC13363'
// svc.info.servicePath // 'customerProfile'
// svc.info.definition // field schema array
// svc.info.status // 'Active' | 'Draft' | ...
// 4. Records CRUD via the bound handle — full Swagger surface.
const list = await svc.records.list({ count: 10, sort: '-_metadata.createdAt' });
const count = await svc.records.count();
const agg = await svc.records.aggregate([{ $count: 'total' }]);
const record = await svc.records.create({ firstName: 'Ada' });
await svc.records.update(record._id, { firstName: 'Ada Lovelace' });
await svc.records.math(record._id, [{ $inc: { visitCount: 1 } }]);
await svc.records.delete(record._id);
// 5. Service lifecycle (pre-bound to svc.serviceId).
await svc.deploy();
await svc.start();
await svc.stop();
// 6. App-scoped sub-clients (no service registry needed).
await app.connectors.list({ category: 'DB' });
await app.flows.list();
await app.flows.publish('FLOW6156');
await app.deployments.start('DG123');
await app.apiKeys.create({ name: 'Test', expiryAfter: 30 });
await app.interactions.list('FLOW6156', { count: 5 });
await dnio.logout();Flat clients (multi-app loops, advanced)
The flat namespaces remain available — every domain client takes (appName, servicePath, ...) explicitly. Use these when you need to operate on multiple apps in one function.
const apps = await dnio.apps.list(); // admin-only
await dnio.records.list('appA', 'studentRecord', { count: 10 });
await dnio.records.list('appB', 'studentRecord', { count: 10 });Token refresh
By default, an expired token causes the next call to throw with 401. Two ways to handle:
- Silent re-login (recommended for kiosks / long sessions): construct with
persistCredentials: true. The SDK stores{username, password}alongside the session in your StorageAdapter and silently callslogin()again before expiry (and on any 401). Trade-off: credentials sit in browser storage; do NOT enable on shared/untrusted devices. - Consumer-handled (recommended for normal apps): pass
onTokenExpired: (err) => routeToLogin(). The SDK fires this callback when it can't transparently refresh. Caller redirects the user to a login screen.
Both are optional. Without either, calls just fail with HttpError(status: 401) after expiry — handle in your own retry logic.
Per-feature (lean imports)
Every feature class is importable through a subpath export so bundlers can tree-shake / drop unused code.
const { HttpClient } = require('@datanimbus/dnio-sdk/core');
const { AuthClient } = require('@datanimbus/dnio-sdk/auth');
const { RecordsClient } = require('@datanimbus/dnio-sdk/records');
const http = new HttpClient({ baseUrl: 'https://your.dnioinstance.io' });
const auth = new AuthClient(http);
await auth.login({ username, password });
const records = new RecordsClient(http);
await records.list('myApp', 'studentRecord');Subpaths available: /core, /auth, /apps, /services, /records, /connectors, /plugins, /flows, /deployments, /data-formats, /api-keys, /interactions, /agents, /workflows.
Capabilities at a glance
| Domain | Entry point | What you can do | What you can't (use MCP / UI) |
|---|---|---|---|
| Authentication | dnio.auth | login, logout, restore session, silent / hooked refresh | — |
| Apps & service registry | dnio.apps, dnio.app(name) | list apps, sticky select-app, list services in app, resolve service by name | — |
| Data Services | app.services, await app.service(name) | list, get, getSchema, deploy, start, stop | create / update definition |
| Records | svc.records.* | full CRUD + count + math + aggregate + bulk + export + file upload/download + hook + simulate | — |
| Connectors | app.connectors | list instances, list marketplace types, create instance | — |
| Plugins | app.plugins | listMarketplace, listInstalled (read-only) | install / update / uninstall |
| Flows (data pipes) | app.flows | list, get, publish, build invocation URL | create / update flow doc / add or remove nodes |
| Deployment groups | app.deployments | list, get, listAvailableFlows, getYamls, start, stop, sync | create / update / delete group, add or remove flows |
| Data formats | app.dataFormats | list, get | create / update / delete |
| API keys | app.apiKeys | list, get, create, update, delete | — |
| Flow Interactions | app.interactions | list runs, get one, per-node state, raw payload at any node | — |
Domain reference
Everything below uses the recommended handle chain. Each section names what comes back so you know what to consume.
1. Authentication
const dnio = new DNIO({
baseUrl: 'https://your.dnioinstance.io',
// session persistence:
storage: new CookieStorageAdapter({ maxAgeSec: 86400 }), // OR LocalStorage / Memory / your own
sessionKey: 'session', // storage key
// expiry handling — pick at most one:
persistCredentials: true, // (1) silent re-login
// onTokenExpired: (err) => routeToLogin(), // (2) consumer-handled
refreshLeadMs: 2 * 60 * 1000 // refresh this many ms before expiry (0 disables)
});
// Restore from storage if present, else login fresh.
if (!(await dnio.initialize())) {
await dnio.login({ username: '[email protected]', password: '...' });
}
dnio.isAuthenticated(); // → boolean
dnio.getToken(); // → JWT string | null
await dnio.logout(); // clears memory + storage + cancels scheduled refreshAuthClient subscribes to the shared HttpClient's tokenExpired event so a 401 anywhere transparently triggers refresh(). If persistCredentials is off and no onTokenExpired is set, the original 401 surfaces as an HttpError.
2. Apps & service registry
// Admin-only: list every app the current user can access.
const apps = await dnio.apps.list({ count: -1, select: '_id,description' });
// → Array<{_id, description, ...}>
// Bind to one app — sync, no network yet.
const app = dnio.app('MCP');
app.name; // 'MCP'
app.loaded; // false until something triggers a registry load
// Lazy-load + read the service registry.
const services = await app.listServices();
// → Array<ServiceEntry>: { serviceId, name, servicePath, toolPrefix, description, definition, status }
// Pre-load explicitly if you'll be calling app.service(...) repeatedly:
await app.load();
app.servicesList; // sync getter — same array as above
await app.refresh(); // drop the cache + reload3. Data Services (definitions)
Read + lifecycle only. Authoring belongs to the MCP / UI.
// At the app level — operate on any service by id.
const all = await app.services.list({ filter: { status: 'Active' } });
// → Array<service document>
const def = await app.services.get('SRVC13363', { draft: false });
const full = await app.services.getSchema('SRVC13363'); // full document incl. definition
await app.services.deploy('SRVC13363'); // PUT /api/a/sm/{app}/service/utils/{id}/deploy
await app.services.start('SRVC13363');
await app.services.stop('SRVC13363');
// At the service level — bound to one service, no id arg.
const svc = await app.service('customerProfile');
svc.serviceId; // 'SRVC13363'
svc.servicePath; // 'customerProfile'
svc.status; // 'Active'
svc.info.definition; // field schema array (length === field count)
await svc.deploy();
await svc.start();
await svc.stop();
const fresh = await svc.getSchema(); // re-fetch full document4. Records (full Swagger surface)
The most-used surface. Every RecordsClient method is exposed on svc.records.* with appName + servicePath pre-bound.
const svc = await app.service('customerProfile');
// ── Read ─────────────────────────────────────────────────────────────────
const list = await svc.records.list({ filter: { status: 'Active' }, sort: '-_metadata.createdAt', count: 20, page: 1, expand: false });
// → Array<record document>
const one = await svc.records.get('REC123', { expand: true, select: 'firstName,email' });
// → record document
const total = await svc.records.count(); // number
const some = await svc.records.count({ status: 'Active' }); // number
// Mongo aggregation pipeline — ad-hoc analytics.
const agg = await svc.records.aggregate([
{ $match: { status: 'Active' } },
{ $group: { _id: '$country', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]);
// → Array<aggregation row>
// ── Write ────────────────────────────────────────────────────────────────
const created = await svc.records.create({ firstName: 'Ada' }, { expireAfter: '7d' });
// → created record (server-enriched: _id, _metadata, ...)
await svc.records.update(created._id, { firstName: 'Ada Lovelace' }, { upsert: false });
// → updated record
await svc.records.delete(created._id); // empty response
// Atomic numeric mutations on a single record.
await svc.records.math(created._id, [
{ $inc: { visitCount: 1, score: 0.5 } },
{ $mul: { score: 1.1 } }
]);
// → mutated record
// ── Bulk ─────────────────────────────────────────────────────────────────
await svc.records.bulkUpdate(['REC1', 'REC2'], { tag: 'archived' }); // → Array<updated record>
await svc.records.bulkUpsert(
{ keys: ['email'], docs: [{ email: 'a@x', name: 'A' }] },
{ update: true, insert: true }
); // → Array<updated + inserted>
await svc.records.bulkDelete(['REC1', 'REC2']); // → empty
// ── Files ────────────────────────────────────────────────────────────────
// fileUpload accepts Blob / File / Buffer / Uint8Array.
const meta = await svc.records.fileUpload(blobOrBuffer, { filename: 'invoice.pdf', encryptionKey: '<optional>' });
// → file metadata: { _id, filename, contentType, length, ... }
// fileDownload returns the raw `Response` so the caller can blob() / arrayBuffer() / pipe to disk.
const res = await svc.records.fileDownload(meta._id, { encryptionKey: '<optional>' });
const buf = await res.arrayBuffer();
// fileMapperCount — count of records associated with a previously-imported mapper file.
await svc.records.fileMapperCount(fileId); // → number
// ── Export ───────────────────────────────────────────────────────────────
const job = await svc.records.exportRecords(
{ filter: '...', select: 'firstName,email', sort: '-_metadata.createdAt', skip: 0, batchSize: 1000 },
{ expand: false }
);
// → export job descriptor with the export file _id
const dl = await svc.records.exportDownload(job._id, { filename: 'customers.csv' });
const text = await dl.text();
// ── Hooks ────────────────────────────────────────────────────────────────
// Manually re-fire the configured webhook for this service.
await svc.records.hook({ event: 'manual' }, { url: '<override URL — optional>' });
// ── Validation / dry-run ─────────────────────────────────────────────────
// Server validates + enriches the doc but does NOT persist. Useful for forms,
// schema discovery, and debugging "why does my create fail?".
const sim = await svc.records.simulate(
{ firstName: 'Ada' },
{ generateId: true, operation: 'POST', select: '...' }
);
// → enriched (un-persisted) document; OR the platform throws HttpError(400) with the field validation list5. Connectors
// Browse marketplace types (each item carries a `fields` schema describing required credentials).
const types = await app.connectors.listMarketTypes({ count: 1000 });
// → Array<{ _id, type, label, category, fields: [{key, label, type, required, encrypted}, ...] }>
// List existing instances (filter by category for picker UIs).
const dbs = await app.connectors.list({ category: 'DB' });
const stgs = await app.connectors.list({ category: 'STORAGE' });
// → Array<{ _id, name, category, type, options, _metadata }>
// Register a new instance.
const inst = await app.connectors.create({
name: 'My MongoDB Prod',
marketItemId: types[0]._id,
values: { connectionString: 'mongodb://...' }
});
// → persisted connector instance6. Data Pipes (flows)
Read + publish only.
const flows = await app.flows.list();
// → Array<{ _id, name, status, inputNode, ... }>
const flow = await app.flows.get('FLOW6156');
// → full flow document
// Public invocation URL for HTTP-triggered flows. Returns null for Timer / non-HTTP triggers.
const inv = app.flows.invocationUrl(flow);
// → { method: 'POST', url: 'https://your.dnioinstance.io/b2b/pipes/MCP/ingest' } | null
await app.flows.publish('FLOW6156');
// → updated flow document (Draft → Active). Required before adding to a deployment group.7. Deployment Groups (Kubernetes)
Read + lifecycle only. Group authoring (create / update / delete / add or remove flows / rename) is out of scope — use the MCP server or platform UI.
// Inventory.
const groups = await app.deployments.list();
// → Array<{ _id, name, status, deployments: [{_id, name, type:'FLOW', version, ...}], ... }>
const grp = await app.deployments.get('DG3017');
// → full group document
// Discover what could be added to a NEW group (published flows not currently bound).
const avail = await app.deployments.listAvailableFlows();
// → Array<flow>
// Actual K8s manifests the platform generated for this group.
const yamls = await app.deployments.getYamls('DG3017');
// → Array<manifest> OR object keyed by manifest kind — depends on platform version
// Lifecycle. K8s ops are async on the platform side (~10s settle time).
await app.deployments.start('DG3017'); // PUT /utils/{id}/start → spin up pods
await app.deployments.stop('DG3017'); // PUT /utils/{id}/stop → tear down pods (definition preserved)
await app.deployments.sync('DG3017'); // PUT /utils/{id}/sync → re-pull latest published flows8. Plugins (workflow nodes)
Read-only catalog browse. Install / update / uninstall is out of scope.
const market = await app.plugins.listMarketplace({ page: 1, count: 50, sort: 'label' });
// → Array<{ _id, marketId, label, type, version, ... }>
const installed = await app.plugins.listInstalled({ count: 200 });
// → Array<{ _id, name, type, version, ... }>Note: the marketplace
_idis NOT the same as the installed_id. Install returns a different id for the per-app copy.
9. Data formats
Read-only.
const formats = await app.dataFormats.list({ count: 50 });
// → Array<format summary>
const full = await app.dataFormats.get('DF1234');
// → full format document with attribute / HRSF section definitions10. API keys
Full lifecycle. The JWT comes back ONLY ONCE in the create response — surface it to the user immediately.
const keys = await app.apiKeys.list();
// → Array<{ _id, name, status, expiryAfterDate, roles: [...] }> — JWT NOT included
const meta = await app.apiKeys.get('API1202');
// → key document — JWT NOT included
const created = await app.apiKeys.create({ name: 'dashboard-test', expiryAfter: 30 });
// → { ..., apiKey: '<JWT — shown only once>' } — SAVE IT NOW
// Update is a full-document PUT — round-trip server-managed fields like
// tokenHash, expiryAfterDate. Use the `redactApiKey` helper to strip the JWT
// from a fetched doc before mutating.
const { redactApiKey } = require('dnio');
const doc = await app.apiKeys.get('API1202');
await app.apiKeys.update('API1202', { ...redactApiKey(doc), status: 'Disabled' });
// Permanent. Prefer status='Disabled' if you may need to re-enable.
await app.apiKeys.delete('API1202');11. Flow Interactions (runtime observability)
Read-only debug / audit surface. Each interaction is one flow run; each run has one state per node that executed; each state has actual incoming + outgoing payload accessible separately.
// 1. List runs for a flow.
const runs = await app.interactions.list('FLOW6272', {
page: 1,
count: 30,
sort: '-_metadata.createdAt',
filter: { status: 'ERROR' } // or { '_metadata.createdAt': { $gte: '2026-01-01' } }
});
// → Array<{ _id, status:'SUCCESS'|'ERROR', txnId, _metadata: {createdAt, ...} }>
// 2. Full metadata for one run (headers, params, query, txn ids, parent linkage).
const det = await app.interactions.get('FLOW6272', runs[0]._id);
// 3. Per-node trace — schema summaries only (NOT actual payloads).
const states = await app.interactions.state('FLOW6272', runs[0]._id);
// → Array<NodeState>: { nodeId, status, statusCode, incoming: {schema}, outgoing: {schema}, _metadata: {...} }
// Ordered by execution. The flow path is `states.map(s => s.nodeId)`.
// 4. Actual payload at one specific node.
const data = await app.interactions.nodeData('FLOW6272', runs[0]._id, 'route_request');
// → { nodeId, status, statusCode, incoming, outgoing, _metadata: {...} }
// incoming + outgoing carry the real data the runtime saw at that step.UI integration
The SDK is browser-first. Drop it into any framework — single shared instance per app, exposed via your DI / context primitive of choice.
Plain HTML / vanilla JS
<script type="module">
import { DNIO } from 'https://esm.sh/@datanimbus/dnio-sdk';
const dnio = new DNIO({ baseUrl: 'https://your.dnioinstance.io' });
document.querySelector('#login').addEventListener('submit', async (e) => {
e.preventDefault();
await dnio.login({
username: e.target.username.value,
password: e.target.password.value
});
const app = dnio.app('MCP');
const svc = await app.service('customerProfile');
const rows = await svc.records.list({ count: 20 });
document.querySelector('#rows').textContent = JSON.stringify(rows, null, 2);
});
</script>React
One DNIO per app. Share via context.
// dnio-context.js
import { createContext, useContext, useMemo } from 'react';
import { DNIO } from '@datanimbus/dnio-sdk';
const DnioContext = createContext(null);
export function DnioProvider({ baseUrl, children }) {
const dnio = useMemo(() => new DNIO({
baseUrl,
persistCredentials: true,
onTokenExpired: () => { window.location.href = '/login'; }
}), [baseUrl]);
return <DnioContext.Provider value={dnio}>{children}</DnioContext.Provider>;
}
export const useDnio = () => useContext(DnioContext);// CustomerTable.jsx
import { useEffect, useState } from 'react';
import { useDnio } from './dnio-context';
export function CustomerTable() {
const dnio = useDnio();
const [rows, setRows] = useState([]);
useEffect(() => {
let cancelled = false;
(async () => {
if (!dnio.isAuthenticated()) await dnio.initialize();
const app = dnio.app('MCP');
const svc = await app.service('customerProfile');
const list = await svc.records.list({ count: 50, sort: '-_metadata.createdAt' });
if (!cancelled) setRows(list);
})();
return () => { cancelled = true; };
}, [dnio]);
return (
<table>
<tbody>
{rows.map((r) => (
<tr key={r._id}>
<td>{r.firstName}</td>
<td>{r.email}</td>
</tr>
))}
</tbody>
</table>
);
}// App.jsx — wire it all up
import { DnioProvider } from './dnio-context';
import { CustomerTable } from './CustomerTable';
export default function App() {
return (
<DnioProvider baseUrl="https://your.dnioinstance.io">
<CustomerTable />
</DnioProvider>
);
}Vue 3
Inject the SDK at app boot, consume from any component.
// main.js
import { createApp } from 'vue';
import { DNIO } from '@datanimbus/dnio-sdk';
import App from './App.vue';
const dnio = new DNIO({ baseUrl: 'https://your.dnioinstance.io', persistCredentials: true });
const app = createApp(App);
app.provide('dnio', dnio);
app.mount('#app');<!-- CustomerTable.vue -->
<script setup>
import { inject, ref, onMounted } from 'vue';
const dnio = inject('dnio');
const rows = ref([]);
onMounted(async () => {
if (!dnio.isAuthenticated()) await dnio.initialize();
const app = dnio.app('MCP');
const svc = await app.service('customerProfile');
rows.value = await svc.records.list({ count: 50 });
});
</script>
<template>
<ul><li v-for="r in rows" :key="r._id">{{ r.firstName }} — {{ r.email }}</li></ul>
</template>Next.js / SSR
The SDK is browser-only — DO NOT instantiate it in server components. Wrap it in a client-side context provider:
// app/dnio-provider.tsx
'use client';
import { DnioProvider } from '@/lib/dnio-context';
export function DnioRoot({ children }) {
return <DnioProvider baseUrl={process.env.NEXT_PUBLIC_DNIO_BASE_URL}>{children}</DnioProvider>;
}
// app/layout.tsx (server component)
import { DnioRoot } from './dnio-provider';
export default function RootLayout({ children }) {
return <html><body><DnioRoot>{children}</DnioRoot></body></html>;
}For SSR session restore, use CookieStorageAdapter so the server can read the same JWT cookie:
new DNIO({
baseUrl: '...',
storage: new CookieStorageAdapter({ maxAgeSec: 86400, sameSite: 'Lax' })
});Login form pattern
function LoginForm() {
const dnio = useDnio();
const [err, setErr] = useState(null);
async function onSubmit(e) {
e.preventDefault();
setErr(null);
try {
await dnio.login({
username: e.target.username.value,
password: e.target.password.value
});
window.location.href = '/dashboard';
} catch (ex) {
setErr(ex.body?.message || ex.message);
}
}
return (
<form onSubmit={onSubmit}>
<input name="username" required />
<input name="password" type="password" required />
<button type="submit">Sign in</button>
{err && <p style={{color: 'red'}}>{err}</p>}
</form>
);
}App / service picker pattern
function AppPicker({ onSelect }) {
const dnio = useDnio();
const [apps, setApps] = useState([]);
useEffect(() => {
dnio.apps.list() // admin-only
.then(setApps)
.catch((e) => e.status === 403 ? setApps([{ _id: 'MCP', description: 'enter manually' }]) : null);
}, [dnio]);
return (
<select onChange={(e) => onSelect(e.target.value)}>
{apps.map((a) => <option key={a._id} value={a._id}>{a._id}</option>)}
</select>
);
}
function ServicePicker({ appName, onPick }) {
const dnio = useDnio();
const [services, setServices] = useState([]);
useEffect(() => {
if (!appName) return;
dnio.app(appName).listServices().then(setServices);
}, [dnio, appName]);
return (
<select onChange={(e) => onPick(e.target.value)}>
{services.map((s) => (
<option key={s.serviceId} value={s.name}>{s.name} ({s.status})</option>
))}
</select>
);
}Storage adapters
The auth module persists the session via a pluggable adapter. Pass one to new DNIO({ storage }) or new AuthClient(http, { storage }):
| Adapter | Use when |
|---|---|
| LocalStorageAdapter | Browser, default. Survives reloads. |
| CookieStorageAdapter | Browser SSR / cross-tab. Tunable Path/Domain/SameSite/Secure/Max-Age. |
| MemoryStorage | Node, tests, ephemeral. The auto-default outside browsers. |
| Your own | Implement { get(key), set(key, value), remove(key) } (sync or async). |
If no adapter is passed, defaultStorage() picks LocalStorageAdapter in browsers and MemoryStorage everywhere else.
Error handling
Every error thrown by the SDK extends DnioError. The most useful subclasses:
| Class | When |
|---|---|
| HttpError | Non-2xx response, network failure, or timeout. Carries status, body, path. |
| AuthError | Login failures, invalid credentials, missing tokens. |
| NotInitializedError | Calling an authenticated method before .initialize() / .login() has set a token. |
const { HttpError, AuthError } = require('dnio');
try {
await svc.records.create({ firstName: 'Ada' });
} catch (err) {
if (err instanceof HttpError) {
// err.status — HTTP status code (0 for network failure)
// err.path — request path
// err.body — parsed response body (often {message: '...'})
if (err.status === 400) {
// Platform validation — show err.body.message to the user.
} else if (err.status === 403) {
// Permission denied — surface "you don't have access" UI.
} else if (err.status === 404) {
// Not found — record / service / app missing.
} else {
// 5xx — retry or alert.
}
} else if (err instanceof AuthError) {
// Login failed, missing token, refresh failed.
routeToLogin();
}
}Tip: call svc.records.simulate(doc, { generateId: true, operation: 'POST' }) before a create() to discover what fields are mandatory — the platform returns a 400 with field : Mandatory Field for every missing required field, no record persisted.
Adding a new feature package
mkdir packages/<name>and add apackage.json(copy any sibling).mkdir packages/<name>/srcand writeindex.jsexporting aClientclass that takes the sharedhttp.- Add
@dnio/<name>topackages/sdk/package.jsondependencies and import + mount it inpackages/sdk/src/index.js. - Run
npm installat the repo root to wire workspaces. - Add JSDoc on every public method — type signatures + parameter docs are how consumers discover the surface.
Contracts
- JS only. No TypeScript, no build step. Source is shipped as-is via the
files: ["src"]whitelist. - CommonJS. All packages use
require/module.exports. Modern bundlers (Vite, Next, webpack) handle CJS fine. - Node 18+ required for native
fetch. - Single
HttpClientshared across feature clients — auth lives there, every call inherits it. - No per-package side effects. Importing a feature does not perform network I/O until you call a method on its class.
License
ISC.
