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.
- 🪄 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, andlimitdirectly in your components, just like in your backend code. - 🛠️ Lifecycle Hooks: Handle
onLoadStartandonCompletefor fine-grained control over the loading state.
npm install driftql-react
# or
yarn add driftql-reactBackend installation:
composer require tonka/driftqlThe 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:configThis command will create/overwrite:
drift.config.js(Frontend configuration)config/driftql.php(Backend configuration)
Once generated, you can review and customize the files below.
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);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,
],
];// App.tsx
import { ElegantProvider } from 'driftql-react';
function App() {
return (
<ElegantProvider config={{
timeout: 10000,
cache: true
}}>
<YourApp />
</ElegantProvider>
);
}// 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>
);
}// 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>
);
}// 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})`;
}
}// 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');
}// 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');
}| 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 |
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.
MIT