npm package: @promakeai/dbreact
Description: React client for schema-driven multi-language database
Author: Promake Inc.
Latest version: 1.2.0

# @promakeai/dbreact

React hooks and providers for schema-driven, multi-language databases. Type-safe, with automatic translation support.

## Features

- **Type-Safe Hooks** - Generic React hooks with generated TypeScript types
- **Multi-Language Support** - Automatic translation queries with fallback
- **Browser SQLite** - sql.js WASM adapter for offline-first apps
- **REST API Adapter** - Remote database access with dynamic Bearer auth
- **React Query Integration** - Built-in caching, loading states, optimistic updates
- **Zero-Config Language Switching** - Change language, queries refetch automatically
- **MongoDB-Style Queries** - Intuitive filter syntax (`$gt`, `$in`, `$like`, `$contains`, etc.)

## Installation

```bash
npm install @promakeai/dbreact @tanstack/react-query
```

**Peer Dependencies:**
- `react` >= 18.0.0
- `@tanstack/react-query` >= 5.0.0

## Quick Start

### 1. Setup Adapter

```tsx
import { SqliteAdapter } from '@promakeai/dbreact';
import { parseJSONSchema } from '@promakeai/dbreact';
import schemaJson from './schema.json';

const schema = parseJSONSchema(schemaJson as any);

const adapter = new SqliteAdapter({
  storageKey: 'myapp-db',  // localStorage key for persistence
  schema,
});
```

### 2. Wrap App with Provider

```tsx
import { DbProvider } from '@promakeai/dbreact';

function App() {
  const [lang, setLang] = useState('en');

  return (
    <DbProvider
      adapter={adapter}
      lang={lang}
      fallbackLang="en"
      autoConnect={true}
    >
      <MyApp />
    </DbProvider>
  );
}
```

### 3. Use Hooks

```tsx
import { useDbList, useDbGet, useDbCreate } from '@promakeai/dbreact';

function ProductList() {
  // List with filters
  const { data: products, isLoading } = useDbList('products', {
    where: { stock: { $gt: 0 } },
    orderBy: [{ field: 'name', direction: 'ASC' }],
    limit: 20,
  });

  // Create mutation
  const createProduct = useDbCreate('products');

  const handleCreate = () => {
    createProduct.mutate({
      sku: 'NEW-001',
      price: 99.99,
    });
  };

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      {products?.map(p => <ProductCard key={p.id} product={p} />)}
      <button onClick={handleCreate}>Add Product</button>
    </div>
  );
}
```

---

## API Reference

### DbProvider

Main provider component that wraps your application.

```tsx
<DbProvider
  adapter={adapter}
  schema={schema}
  lang="tr"
  fallbackLang="en"
  autoConnect={true}
>
  <App />
</DbProvider>
```

**Props:**

| Prop | Type | Required | Default | Description |
|------|------|----------|---------|-------------|
| `adapter` | `IDataAdapter` | Yes | - | Database adapter instance |
| `schema` | `SchemaDefinition` | No | - | Enables populate + typed serialization |
| `lang` | `string` | No | `'en'` | Current language code |
| `fallbackLang` | `string` | No | `'en'` | Fallback language |
| `autoConnect` | `boolean` | No | `true` | Auto-connect on mount |
| `queryClient` | `QueryClient` | No | Internal | Provide a custom React Query client |
| `children` | `ReactNode` | Yes | - | Child components |

---

### Query Hooks

#### useDbList

Fetch multiple records with filtering, pagination, and sorting.

```tsx
const { data, isLoading, error, refetch } = useDbList<Product>('products', {
  where: { stock: { $gt: 0 } },
  orderBy: [{ field: 'price', direction: 'DESC' }],
  limit: 20,
  offset: 0,
  enabled: true,
});
```

**Options:**

| Option | Type | Description |
|--------|------|-------------|
| `where` | `object` | MongoDB-style filter conditions |
| `orderBy` | `array` | Sort order `[{ field, direction }]` |
| `limit` | `number` | Max records to return |
| `offset` | `number` | Skip records |
| `populate` | `PopulateOption` | Resolve foreign key references (string, array, or object) |
| `enabled` | `boolean` | Enable/disable query |

#### useDbGet

Fetch single record by ID.

```tsx
const { data: product, isLoading } = useDbGet<Product>('products', productId, {
  populate: ['categoryId'],
  enabled: !!productId,
});
```

Find-one style (by where clause):

```tsx
const { data: product } = useDbGet<Product>('products', {
  where: { slug: 'my-product' },
  enabled: !!slug,
  populate: { categoryId: true },
});
```

---

### Mutation Hooks

#### useDbCreate

Create a new record.

```tsx
const createProduct = useDbCreate<Product>('products');

createProduct.mutate(
  { sku: 'NEW-001', price: 99.99 },
  {
    onSuccess: (data) => console.log('Created:', data),
    onError: (error) => console.error(error),
  }
);
```

#### useDbUpdate

Update an existing record.

```tsx
const updateProduct = useDbUpdate<Product>('products');

updateProduct.mutate(
  { id: productId, data: { price: 89.99 } },
  {
    onSuccess: () => console.log('Updated'),
  }
);
```

#### useDbDelete

Delete a record.

```tsx
const deleteProduct = useDbDelete('products');

deleteProduct.mutate(productId, {
  onSuccess: () => console.log('Deleted'),
});
```

---

### Context Hooks

#### useDb

Access database connection status.

```tsx
const { adapter, isConnected, error } = useDb();

if (!isConnected) return <div>Connecting...</div>;
```

#### useAdapter

Access adapter directly for raw queries.

```tsx
const adapter = useAdapter();

const handleRawQuery = async () => {
  const results = await adapter.raw('SELECT * FROM products WHERE price > ?', [100]);
  console.log(results);
};
```

#### useDbLang

Access and control language settings.

```tsx
const { lang, fallbackLang, setLang } = useDbLang();

// Language switcher
<select value={lang} onChange={e => setLang(e.target.value)}>
  <option value="en">English</option>
  <option value="tr">Turkce</option>
  <option value="de">Deutsch</option>
</select>
```

---

### SqliteAdapter

Browser-based SQLite storage using sql.js (WebAssembly).

```tsx
const adapter = new SqliteAdapter({
  storageKey: 'myapp-db',     // localStorage key for persistence
  schema: mySchema,           // Optional: schema definition
  initialData: seedData,      // Optional: initial seed data
});
```

**Features:**
- Automatic persistence to localStorage
- Full SQL support
- Works offline
- WASM-based (no native dependencies)

**Limitations:**
- Data stored in browser only
- localStorage limit: ~5-10MB (browser dependent)
- Best for < 10,000 records

### RestAdapter

REST API adapter for remote database access. Supports static tokens and dynamic `getToken` callbacks.

```tsx
import { RestAdapter } from '@promakeai/dbreact';

const adapter = new RestAdapter({
  baseUrl: 'https://api.example.com',
  databasePrefix: '/database',
  schema,
  defaultLang: 'en',
  // Dynamic token — called on every request
  getToken: () => {
    try {
      const raw = localStorage.getItem('auth-storage');
      return raw ? JSON.parse(raw)?.state?.tokens?.accessToken ?? null : null;
    } catch { return null; }
  },
});
```

**Config:**

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `baseUrl` | `string` | Yes | API base URL |
| `databasePrefix` | `string` | No | Endpoint prefix (default: `/database`) |
| `token` | `string` | No | Static Bearer token |
| `getToken` | `() => string \| null` | No | Dynamic token getter (priority over `token`) |
| `schema` | `SchemaDefinition` | No | Schema for translation support |
| `defaultLang` | `string` | No | Default language |
| `headers` | `Record<string, string>` | No | Custom headers |
| `timeout` | `number` | No | Request timeout in ms (default: 30000) |

---

## Query Options

### MongoDB-Style Filters

```typescript
// Comparison
{ price: { $gt: 100 } }              // > 100
{ price: { $gte: 100 } }             // >= 100
{ price: { $lt: 100 } }              // < 100
{ price: { $lte: 100 } }             // <= 100
{ price: { $ne: 100 } }              // != 100
{ price: { $eq: 100 } }              // = 100

// Array
{ id: { $in: [1, 2, 3] } }           // IN (1, 2, 3)
{ id: { $nin: [1, 2, 3] } }          // NOT IN (1, 2, 3)

// String
{ name: { $like: '%shirt%' } }       // LIKE '%shirt%'
{ name: { $notLike: '%test%' } }     // NOT LIKE '%test%'

// Range
{ price: { $between: [10, 100] } }   // BETWEEN 10 AND 100

// Null
{ description: { $isNull: true } }   // IS NULL
{ description: { $isNull: false } }  // IS NOT NULL

// JSON array contains
{ tags: { $contains: "sale" } }
{ tags: { $containsAny: ["sale", "new"] } }

// Logical
{ $and: [
  { price: { $gt: 50 } },
  { stock: { $gt: 0 } }
]}

{ $or: [
  { category: 'shirts' },
  { category: 'pants' }
]}

{ $not: { price: { $lt: 100 } } }    // NOT (price < 100)
```

### Query Interface

```typescript
interface QueryOptions {
  where?: Record<string, unknown>;
  orderBy?: Array<{
    field: string;
    direction: 'ASC' | 'DESC';
  }>;
  limit?: number;
  offset?: number;
  populate?: PopulateOption;
  lang?: string;
  fallbackLang?: string;
}
```

---

## Multi-Language Support

### Schema with Translatable Fields

```json
{
  "languages": ["en", "tr", "de"],
  "tables": {
    "products": {
      "id": { "type": "id" },
      "price": { "type": "decimal" },
      "name": { "type": "string", "translatable": true },
      "description": { "type": "text", "translatable": true }
    }
  }
}
```

### Automatic Translation

Queries automatically use the current language from DbProvider:

```tsx
function ProductList() {
  // Automatically fetches in current language
  const { data: products } = useDbList('products');

  // products[0].name is in Turkish if lang='tr'
  // Falls back to English if Turkish translation missing
}
```

### Language Switching

When you change language, React Query automatically refetches all queries:

```tsx
function LanguageSwitcher() {
  const { lang, setLang } = useDbLang();

  return (
    <select value={lang} onChange={e => setLang(e.target.value)}>
      <option value="en">English</option>
      <option value="tr">Turkce</option>
    </select>
  );
}
```

---

## Generated Types + Generic Hooks

Generate runtime schema and TypeScript interfaces:

```bash
dbcli generate --schema ./schema.json --output ./src/db
```

`dbcli generate` writes `schema.json` and `types.ts`. React hooks are imported from `@promakeai/dbreact`:

```tsx
import { useDbList, useDbGet, useDbCreate } from '@promakeai/dbreact';
import type { DbProduct, DbProductInput } from './db/types';

function ProductManager() {
  const { data: products } = useDbList<DbProduct>('products', {
    where: { stock: { $gt: 0 } },
  });

  const { data: product } = useDbGet<DbProduct>('products', 1);

  const createProduct = useDbCreate<DbProduct, DbProductInput>('products');

  return null;
}
```

---

## Advanced Usage

### Custom React Query Options

```tsx
const { data } = useDbList('products', {
  where: { active: true },
  limit: 50,
});
```

### Direct Adapter Access

For complex queries not covered by hooks:

```tsx
function AdvancedSearch() {
  const adapter = useAdapter();
  const [results, setResults] = useState([]);

  const handleSearch = async (query: string) => {
    const products = await adapter.list('products', {
      where: {
        $or: [
          { name: { $like: `%${query}%` } },
          { description: { $like: `%${query}%` } },
        ],
      },
      limit: 50,
    });
    setResults(products);
  };

  return <SearchInput onSearch={handleSearch} />;
}
```

### Optimistic Updates

```tsx
function ProductPrice({ product }) {
  const queryClient = useQueryClient();
  const updateProduct = useDbUpdate('products');

  const handlePriceChange = async (newPrice: number) => {
    // Optimistic update
    queryClient.setQueryData(
      ['products', 'detail', product.id],
      { ...product, price: newPrice }
    );

    await updateProduct.mutateAsync({
      id: product.id,
      data: { price: newPrice },
    });
  };

  return <PriceInput value={product.price} onChange={handlePriceChange} />;
}
```

---

## Examples

### E-Commerce Product List

```tsx
function Shop() {
  const { lang } = useDbLang();
  const [category, setCategory] = useState(null);

  const { data: products, isLoading } = useDbList('products', {
    where: category ? { categoryId: category } : {},
    orderBy: [{ field: 'name', direction: 'ASC' }],
  });

  if (isLoading) return <Spinner />;

  return (
    <div>
      <CategoryFilter onSelect={setCategory} />
      <ProductGrid products={products} />
    </div>
  );
}
```

### Multi-Language Blog

```tsx
function BlogPost({ slug }: { slug: string }) {
  const { data: posts } = useDbList('posts', {
    where: { slug, published: true },
    limit: 1,
  });

  const post = posts?.[0];
  if (!post) return <NotFound />;

  return (
    <article>
      <h1>{post.title}</h1>         {/* Auto-translated */}
      <div>{post.content}</div>     {/* Auto-translated */}
    </article>
  );
}
```

### Real-Time Search

```tsx
function ProductSearch() {
  const adapter = useAdapter();
  const [query, setQuery] = useState('');

  const { data: results } = useQuery({
    queryKey: ['search', query],
    queryFn: () => adapter.list('products', {
      where: {
        $or: [
          { name: { $like: `%${query}%` } },
          { sku: { $like: `%${query}%` } },
        ],
      },
      limit: 10,
    }),
    enabled: query.length > 2,
  });

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <SearchResults results={results} />
    </div>
  );
}
```

---

## TypeScript Support

Full TypeScript support with generated types:

```typescript
import type { DbProduct, DbProductInput } from './db/types';

// Type-safe queries
const products: DbProduct[] = await adapter.list('products');

// Type-safe creates
const newProduct: DbProductInput = {
  sku: 'SHIRT-001',
  price: 99.99,
};

await adapter.create('products', newProduct);
```

---

## Performance Tips

1. **Use pagination** for large datasets:
   ```tsx
   const { data } = useDbList('products', { limit: 20, offset: page * 20 });
   ```

2. **Enable query conditionally**:
   ```tsx
   const { data } = useDbGet('products', id, { enabled: !!id });
   ```

3. **Prefetch next page**:
   ```tsx
   queryClient.prefetchQuery({
     queryKey: ['products', 'list', { offset: (page + 1) * 20 }],
     queryFn: () => adapter.list('products', { offset: (page + 1) * 20 }),
   });
   ```

---

## Troubleshooting

**Production crash: `jsxDEV is not a function`**
- Run `bun run test:build-runtime` before publishing.
- This verifies `dist/index.js` does not include `react/jsx-dev-runtime`.

**"useDbLang must be used within a DbProvider"**
- Ensure component is wrapped in DbProvider

**Queries return empty results**
- Check adapter is connected: `const { isConnected } = useDb()`
- Verify data exists in database
- Check query filters are correct

**Translations not working**
- Ensure schema has `.translatable()` fields
- Run `dbcli generate` to create translation tables
- Check translation records exist in `{table}_translations`

---

## Related Packages

- [@promakeai/orm](../orm) - Core ORM with query builder
- [@promakeai/dbcli](../dbcli) - CLI tool for database operations

## License

MIT