@mounaji_npm/database
v0.4.4
Published
Adapter-driven database layer with cache, hooks, and repositories for Mounaji SaaS applications
Downloads
545
Maintainers
Readme
@mounaji_npm/database
Adapter-driven database layer for Mounaji SaaS applications. Ships with Supabase and REST adapters. Features SWR-style React hooks, LRU cache with TTL and tag invalidation, real-time subscriptions, and a repository pattern.
Install
npm install @mounaji_npm/database
# If using Supabase adapter:
npm install @supabase/supabase-jsQuick Start
// app/providers.jsx
import { createClient } from '@supabase/supabase-js';
import { createSupabaseAdapter, DatabaseProvider } from '@mounaji_npm/database';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
);
const adapter = createSupabaseAdapter(supabase);
export function Providers({ children }) {
return (
<DatabaseProvider
adapter={adapter}
cacheOptions={{ ttl: 60_000, staleTime: 30_000, persist: true }}
>
{children}
</DatabaseProvider>
);
}// Usage in a component
import { useQuery, useMutation } from '@mounaji_npm/database';
function UserList({ orgId }) {
const { data, loading, error } = useQuery({
key: ['users', orgId],
table: 'users',
query: (qb) => qb.eq('org_id', orgId).order('name'),
tags: ['users'],
});
const { mutate: createUser } = useMutation({
table: 'users',
operation: 'insert',
invalidateTags: ['users'],
});
if (loading) return <p>Loading…</p>;
return (
<>
{data?.map(u => <div key={u.id}>{u.name}</div>)}
<button onClick={() => createUser({ name: 'New User', org_id: orgId })}>
Add User
</button>
</>
);
}Exports
| Export | Description |
|---|---|
| DatabaseProvider | React context — wraps your app with an adapter |
| useDatabase() | Access adapter, cache, and from() query factory |
| useQuery(opts) | SWR-style read hook with cache, deduplication, background refresh |
| useMutation(opts) | Write hook with automatic cache invalidation |
| useRealtime(opts) | Real-time CDC subscription (Supabase only) |
| createSupabaseAdapter(client) | Supabase adapter factory |
| createRestAdapter(opts) | REST adapter factory |
| QueryCache | Standalone LRU cache primitive |
| QueryBuilder | Fluent query builder |
| RepositoryBase | Base class for domain repositories |
useQuery
SWR-style fetching — caches results, deduplicates in-flight requests, revalidates in the background when stale.
const { data, loading, error, isStale, refetch, invalidate } = useQuery({
key: ['users', orgId], // unique cache key
table: 'users',
query: (qb) => qb
.select('id, name, email, role')
.eq('org_id', orgId)
.order('created_at', { ascending: false })
.limit(50),
ttl: 60_000, // cache TTL in ms (default: 5 min)
staleTime: 20_000, // serve stale + revalidate after (default: 30s)
tags: ['users'], // group key for tag-based invalidation
enabled: !!orgId, // skip fetch when false
});useMutation
// Insert
const { mutate, loading } = useMutation({
table: 'users',
operation: 'insert', // 'insert' | 'update' | 'upsert' | 'delete'
invalidateTags: ['users'], // invalidates all useQuery hooks with this tag
onSuccess: () => toast.success('Created'),
onError: (e) => toast.error(e.message),
});
await mutate({ name: 'Alice', org_id: orgId });
// Update
const { mutate: update } = useMutation({
table: 'users',
operation: 'update',
filters: [{ column: 'id', op: 'eq', value: userId }],
invalidateTags: ['users'],
});
await update({ role: 'admin' });useRealtime (Supabase only)
useRealtime({
table: 'messages',
filter: `channel_id=eq.${channelId}`,
onInsert: ({ new: msg }) => setMessages(prev => [...prev, msg]),
onUpdate: ({ new: msg }) => setMessages(prev => prev.map(m => m.id === msg.id ? msg : m)),
onDelete: ({ old: msg }) => setMessages(prev => prev.filter(m => m.id !== msg.id)),
enabled: !!channelId,
});Repository Pattern
import { RepositoryBase } from '@mounaji_npm/database';
class UserRepository extends RepositoryBase {
constructor(adapter) { super(adapter, 'users'); }
findByOrg(orgId) {
return this.query()
.select('id, name, email, role')
.eq('org_id', orgId)
.eq('active', true)
.order('name')
.execute();
}
activate(id) { return this.updateById(id, { active: true }); }
deactivate(id) { return this.updateById(id, { active: false }); }
}import { useDatabase } from '@mounaji_npm/database';
import { useMemo } from 'react';
function useUserRepo() {
const { adapter } = useDatabase();
return useMemo(() => new UserRepository(adapter), [adapter]);
}Fluent Query Builder
const { from } = useDatabase();
const { data } = await from('events')
.select('type, count(*)')
.eq('org_id', orgId)
.gte('created_at', startDate)
.order('count', { ascending: false })
.limit(10)
.execute();Available filters: .eq .neq .gt .gte .lt .lte .like .ilike .is .in .contains .textSearch
DatabaseProvider Options
<DatabaseProvider
adapter={adapter}
cacheOptions={{
ttl: 300_000, // 5 min — entry expiry
staleTime: 30_000, // 30s — serve stale + revalidate
maxSize: 200, // LRU eviction threshold
persist: false, // hydrate/flush from localStorage
storageKey: 'mn_db_cache', // localStorage key
}}
>REST Adapter
For non-Supabase backends:
import { createRestAdapter, DatabaseProvider } from '@mounaji_npm/database';
const adapter = createRestAdapter({
baseUrl: 'https://api.example.com',
getToken: () => localStorage.getItem('token'),
headers: { 'X-Tenant': orgId },
timeout: 30_000,
});Note:
useRealtimeis not supported with the REST adapter.
Custom Adapter
Implement the MounajiAdapter interface for any backend:
const myAdapter = {
provider: 'my-db',
async select(table, { columns, filters, orders, limit, offset, single }) {
return { data: [], count: 0 };
},
async insert(table, data) { return { data }; },
async update(table, data, filters) { return { data }; },
async upsert(table, data, opts) { return { data }; },
async delete(table, filters) { return { data: null }; },
async rpc(fn, params) { return { data: null }; },
subscribe(table, opts, callback) { return { unsubscribe: () => {} }; },
getClient() { return null; },
};Full API reference with all filter methods and cache management: DOCS.md
