Skip to content

Repository files navigation

DriftQL React 🚀

The bridge between your Tonka Framework application and React. Seamlessly expose your Elegant ORM models to the frontend with a powerful, secure, and declarative API.

🌟 Features

  • 🪄 Auto-Data Binding: Inject ORM data directly into DOM attributes (e.g., auto-fill an img src).
  • ⚛️ Flexible Usage: Supports both simple attribute injection and advanced Render Props with loading/error states.
  • 🔒 Secure by Design: Built-in support for API keys, model whitelisting, and strict SQL sanitization via your DriftQL backend.
  • 🎯 Fluent Querying: Use where, orderBy, and limit directly in your components, just like in your backend code.
  • 🛠️ Lifecycle Hooks: Handle onLoadStart and onComplete for fine-grained control over the loading state.

📦 Installation

npm install driftql-react
# or
yarn add driftql-react

Backend installation:

composer require tonka/driftql

⚙️ Configuration

Auto-Configuration (Recommended)

The Tonka Framework provides a command to generate both the frontend and backend configuration files automatically for you.

Run the following command in your terminal:

php tonka driftql:config

This command will create/overwrite:

  1. drift.config.js (Frontend configuration)
  2. config/driftql.php (Backend configuration)

Once generated, you can review and customize the files below.


1. Frontend Configuration

The generated drift.config.js file will look like this:

// drift.config.js
export default {
  // The base URL of your DriftQL backend bridge
  baseURL: '/api/bridge',
  
  // Request timeout in milliseconds
  timeout: 5000,
  
  // Cache strategy (RequestCache)
  cache: 'default', 
  
  // The public key required to authenticate requests with the backend
  bridge_public_key: 'd3597f10e13310490809c832b898445e88face0bb7635b846d1b651f62417a29'
};

Then, initialize the package in your entry file (e.g., main.tsx or App.js):

import { Client } from 'driftql-react';
import config from './drift.config';

Client.init(config);

2. Backend Configuration (Tonka / PHP)

The generated config/driftql.php file manages security, whitelisting, and data access policies.

// config/driftql.php
<?php

return [
    /*
    |--------------------------------------------------------------------------
    | DRIFTQL BRIDGE ENABLED
    |--------------------------------------------------------------------------
    */
    'enabled' => env('DRIFTQL_BRIDGE_ENABLED', true),

    /*
    |--------------------------------------------------------------------------
    | DRIFTQL BRIDGE KEY
    |--------------------------------------------------------------------------
    | A secret key that the front-end must provide to access the DriftQL bridge.
    | This is an additional layer of security to prevent unauthorized access.
    */
    'bridge_public_key' => env('DRIFTQL_BRIDGE_KEY', 'your-public-key-here'),

    /*
    |--------------------------------------------------------------------------
    | MODEL WHITELIST
    |--------------------------------------------------------------------------
    | List of models (classes) that the front-end is allowed to query. 
    | If a model is not in this list, the query is rejected immediately.
    */
    'whitelist' => [
        'allowed_models' => [
            \App\Models\User::class,
            \App\Models\Employee::class,
            // Note : \App\Models\Admin is not here, so it is inaccessible via this route
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | AUTHORIZATION (Policies & Row Level Security)
    |--------------------------------------------------------------------------
    | Defines access restrictions by Role and by Model. 
    | The backend automatically applies these "Global Scopes".
    |
    | Format :
    | 'ModelName' => [
    |      'RoleName' => [ Filtering rule ]
    | ]
    |
    | Note: You can specify a class implementing \Tonka\DriftQL\Security\Contract
    | instead of an array. This class exposes the authorize() method.
    */
    'policies' => [
        \App\Models\User::class => [
            // Admins see everything (no filter)
            'admin' => null, 
            
            // Editors cannot see other admins
            'editor' => [
                'column' => 'role',
                'operator' => '!=',
                'value' => 'admin', 
            ],
            
            // Standard users only see themselves
            'user' => [
                'column' => 'id',
                'operator' => '=',
                'value' => 'current_user_id',
            ],
        ],
        
        // Example using a Policy Class
        \App\Models\Post::class => \App\Contracts\PostContract::class,
    ],

    /*
    |--------------------------------------------------------------------------
    | HARDCODED LIMITATION (DoS Protection)
    |--------------------------------------------------------------------------
    | Enforce ceilings to protect database performance.
    */
    'limits' => [
        // Maximum number of records the front end can request at once
        'max_limit' => 100, 
        
        // Default number if the front does not specify a limit
        'default_limit' => 20,
        
        // Maximum number allowed for the offset (to avoid excessively deep pagination)
        'max_offset' => 10000,
    ],

    /*
    |--------------------------------------------------------------------------
    | SQL SANITIZATION
    |--------------------------------------------------------------------------
    | Security guidelines for constructing the request.
    */
    'security' => [
        // The use of "raw SQL" from the front end is strictly prohibited.
        // Only structured "where" clauses (column, operator, binding) are accepted.
        'allow_raw_sql' => false,

        // Checks that the columns requested in 'orderBy' or 'where' actually exist
        // in the database schema.
        'strict_column_check' => true,
    ],
];

🚀 Usage Examples

1. Basic Configuration

// App.tsx
import { ElegantProvider } from 'driftql-react';

function App() {
  return (
    <ElegantProvider config={{ 
      timeout: 10000,
      cache: true
    }}>
      <YourApp />
    </ElegantProvider>
  );
}

2. Using useElegant Hook

// shared/UserList.tsx
import { useElegant } from 'driftql-react';

interface User {
  id: number;
  name: string;
  email: string;
}

function UserList() {
  const { all, find, delete: deleteUser } = useElegant<User>('User');
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    all()
      .then(setUsers)
      .finally(() => setLoading(false));
  }, []);

  const handleDelete = async (id: number) => {
    await deleteUser(id);
    setUsers(users.filter(u => u.id !== id));
  };

  if (loading) return <div>Loading...</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name} - {user.email}
          <button onClick={() => handleDelete(user.id)}>Delete</button>
        </li>
      ))}
    </ul>
  );
}

3. Using useQueryBuilder Hook

// shared/UserSearch.tsx
import { useQueryBuilder } from 'driftql-react';

function UserSearch() {
  const { getBuilder } = useQueryBuilder<User>('User');
  const [results, setResults] = useState<User[]>([]);

  const search = async (term: string) => {
    const builder = getBuilder();
    const data = await builder
      .where({ column: 'name', operator: 'LIKE', value: `%${term}%` })
      .where({ column: 'active', operator: '=', value: true })
      .orderBy('name', 'ASC')
      .get();
    setResults(data);
  };

  return (
    <div>
      <input onChange={(e) => search(e.target.value)} placeholder="Search..." />
      <ul>
        {results.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

4. Define a Model

// models/User.ts
import { Elegant } from 'driftql-react';

export interface UserData {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  status: 'active' | 'inactive' | 'pending';
  created_at: string;
  updated_at: string;
}

export class User extends Elegant implements UserData {
  static resourceName = 'User';
  
  id!: number;
  name!: string;
  email!: string;
  role!: 'admin' | 'user' | 'moderator';
  status!: 'active' | 'inactive' | 'pending';
  created_at!: string;
  updated_at!: string;

  // Méthodes personnalisées
  get isAdmin(): boolean {
    return this.role === 'admin';
  }

  get isActive(): boolean {
    return this.status === 'active';
  }

  get displayName(): string {
    return `${this.name} (${this.email})`;
  }
}

5. CRUD

// CREATE
const newUser = await User.store({
  name: 'John Doe',
  email: 'john@example.com',
  role: 'user',
  status: 'active'
});

// READ
const user = await User.find(123);
const allUsers = await User.all();
const activeUsers = await User
  .where({ column: 'status', operator: '=', value: 'active' })
  .get();

// UPDATE
const updated = await User.update(123, {
  name: 'Jane Doe',
  role: 'admin'
});

// DELETE
await User.delete(123);

// SAVE
const user = new User();
user.name = 'John';
user.email = 'john@example.com';
await user.save();

// REFRESH
const user = await User.find(123);
await user.refresh();

// EXISTS
if (await user.exists()) {
  console.log('User exists');
}

6. Advance Request

// Pagination
const result = await User
  .where({ column: 'status', operator: '=', value: 'active' })
  .paginate(1, 15);

console.log(result.data); // Users
console.log(result.pagination.total); // Total

// Eager Loading
const users = await User
  .with(['posts', 'profile'])
  .where({ column: 'status', operator: '=', value: 'active' })
  .get();

// Join
const posts = await Post
  .join({
    resource: 'User',
    type: 'inner',
    fkey: 'user_id',
    okey: 'id'
  })
  .where({ column: 'users.role', operator: '=', value: 'admin' })
  .get();

// Agregation
const stats = await User
  .groupBy('role')
  .select('role, COUNT(*) as total')
  .get();

// Find Or Fail
try {
  const user = await User.findOrFail(123);
} catch (error) {
  console.error('User not found');
}

// Store Or Fail
try {
  const user = await User.storeOrFail({ name: 'John' });
} catch (error) {
  console.error('Creation failed');
}

📚 API Reference

Category Method Description
CRUD find() Find by ID
findOrFail() Find or error
all() All records
store() Create new record
storeOrFail() Create or error
update() Update a record
updateOrFail() Update or error
delete() Destroy a record
deleteOrFail() Destroy or error
Instance with() Eager-loading
where() Condition WHERE
having() Condition HAVING
savg() Save (create/update)
saveOrFail() Save or error
refresh() Refresh
exists() Verify if exists
toJSON() Convert to JSON
toFormData() Convert to FormData

🔐 Security Note

DriftQL is designed to expose your ORM securely. Ensure your backend whitelist and policies are correctly configured to prevent unauthorized data access. The bridge_public_key ensures that only your frontend application can communicate with the bridge.

📝 License

MIT

About

No description, website, or topics provided.

Resources

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages