Refine integration
@taruvi/refine-providers connects a Refine 5 app to
TaruviBase. Refine's data hooks read and write your data tables, its auth hooks
use TaruviBase hosted sign-in, and useCan and <CanAccess> ask your access
policies what to show.
This guide covers @taruvi/refine-providers 1.3.7. It uses your app's copy of
@taruvi/sdk, a peer dependency, which must be 1.5.4 or later.
Before you begin#
You need:
- A React app with
@refinedev/core5. - A TaruviBase app, with
TARUVI_SITE_URLandTARUVI_APP_SLUGfrom Settings → Connect. - For the example below, a
taskstable, as in the quickstart.
Install the packages#
npm install @refinedev/core @taruvi/refine-providers @taruvi/sdk
Create the TaruviBase client#
The providers share one @taruvi/sdk client. This Vite example reads public
build variables; use your framework's equivalent:
import {Client} from '@taruvi/sdk';
export const taruvi = new Client({
apiUrl: import.meta.env.VITE_TARUVI_SITE_URL,
appSlug: import.meta.env.VITE_TARUVI_APP_SLUG,
});
Import this module when the app starts. Constructing the client is what captures the session token when TaruviBase returns a user from sign-in.
Register the providers and routes#
Use dataProvider as Refine's default and name the others; each resource picks
one with meta.dataProviderName. This example uses React Router through
@refinedev/react-router, and keeps every page except sign-in behind
<Authenticated>:
npm install @refinedev/react-router react-router
import {Authenticated, Refine} from '@refinedev/core';
import routerProvider, {CatchAllNavigate, NavigateToResource} from '@refinedev/react-router';
import {BrowserRouter, Outlet, Route, Routes} from 'react-router';
import {
accessControlProvider,
appDataProvider,
authProvider,
dataProvider,
storageDataProvider,
userDataProvider,
} from '@taruvi/refine-providers';
import {taruvi} from './taruvi-client';
import {Documents} from './pages/documents';
import {LoginPage} from './pages/login';
import {OpenTasks} from './pages/tasks';
const dataProviders = {
default: dataProvider(taruvi),
storage: storageDataProvider(taruvi),
app: appDataProvider(taruvi),
user: userDataProvider(taruvi),
};
// Policy kind for each resource; see "Show only what users may do".
const policyKinds: Record<string, string> = {
tasks: 'datatable:tasks',
documents: 'storage:documents',
};
export function App() {
return (
<BrowserRouter>
<Refine
routerProvider={routerProvider}
dataProvider={dataProviders}
authProvider={authProvider(taruvi)}
accessControlProvider={accessControlProvider(taruvi, {
entityType: (resource) => policyKinds[resource],
})}
resources={[
{name: 'tasks', list: '/tasks'},
{name: 'documents', list: '/documents', meta: {dataProviderName: 'storage'}},
{name: 'users', meta: {dataProviderName: 'user'}},
]}
>
<Routes>
<Route
element={
<Authenticated key="app" fallback={<CatchAllNavigate to="/login" />}>
<Outlet />
</Authenticated>
}
>
<Route index element={<NavigateToResource resource="tasks" />} />
<Route path="/tasks" element={<OpenTasks />} />
<Route path="/documents" element={<Documents />} />
</Route>
<Route
element={
<Authenticated key="sign-in" fallback={<Outlet />}>
<NavigateToResource />
</Authenticated>
}
>
<Route path="/login" element={<LoginPage />} />
</Route>
</Routes>
</Refine>
</BrowserRouter>
);
}
Create the providers once, outside the component, so they aren't rebuilt on
every render. The provider reference lists the
resources and meta options each provider supports.
Use the product examples#
The product guides select these providers explicitly:
dataProviderName | Operations |
|---|---|
default | Database records and queries |
storage | Storage objects and supported file operations |
app | Function and Analytics execution, Secrets reads, app roles and settings |
user | Site-user records and their role/app reads |
Import hooks from @refinedev/core and initialize them inside a component
beneath the configured <Refine>. Product snippets show the hook and its
operation parameters. Invoke mutation calls from an event handler, rather
than while rendering the component.
In Refine 5, useOne exposes its record as result, and useList exposes
result.data and result.total; their loading/error state is in query.
Data mutation hooks expose mutate, mutateAsync, and mutation, including
mutation.isPending, mutation.error, and mutation.data.
Add a sign-in page#
authProvider doesn't render a form. Its login() sends the browser to
TaruviBase hosted sign-in, and the user comes back already signed in.
<CatchAllNavigate> sends signed-out users to /login?to=PATH; this page
returns them to that path afterward:
import {useLogin} from '@refinedev/core';
import type {LoginParams} from '@taruvi/refine-providers';
import {useSearchParams} from 'react-router';
export function LoginPage() {
const {mutate: login, isPending} = useLogin<LoginParams>();
const [searchParams] = useSearchParams();
// Only return to a page on this app. TaruviBase adds the session to the
// callback address, so never let it point elsewhere.
const target = new URL(searchParams.get('to') ?? '/', window.location.origin);
const callbackUrl = target.origin === window.location.origin ? target.href : window.location.origin;
return (
<button type="button" disabled={isPending} onClick={() => login({callbackUrl})}>
Sign in with TaruviBase
</button>
);
}
callbackUrl must be an absolute address. useLogout() clears the session,
sends the user through TaruviBase sign-out, and returns to /login. If
TaruviBase doesn't accept your app's address for the return trip, the user
confirms sign-out on TaruviBase's page instead. See
Authenticate with the JavaScript SDK
for what happens during the redirect, and for the app addresses TaruviBase
accepts requests from.
Read and write data#
The data hooks work as they do with any Refine data provider:
import {useDelete, useList, useUpdate} from '@refinedev/core';
type Task = {id: string; title: string; done: boolean};
export function OpenTasks() {
const {
result: {data: tasks, total},
query: {isLoading},
} = useList<Task>({
resource: 'tasks',
filters: [{field: 'done', operator: 'eq', value: false}],
sorters: [{field: 'title', order: 'asc'}],
pagination: {currentPage: 1, pageSize: 20},
});
const {mutate: update} = useUpdate<Task>();
const {mutate: remove} = useDelete<Task>();
if (isLoading) return <p>Loading…</p>;
return (
<ul aria-label={`${total} open tasks`}>
{tasks.map((task) => (
<li key={task.id}>
{task.title}
<button type="button" onClick={() => update({resource: 'tasks', id: task.id, values: {done: true}})}>
Done
</button>
<button type="button" onClick={() => remove({resource: 'tasks', id: task.id})}>
Delete
</button>
</li>
))}
</ul>
);
}
meta adds TaruviBase features to a query, such as related records, full-text
search, and per-row permission hints:
useList({
resource: 'tasks',
meta: {populate: ['assignee'], search: 'launch', allowedActions: ['update', 'delete']},
});
Filters. A filter whose value is undefined or null is skipped, as
Refine's useTable does, except in a filtered delete, where it fails the
request. An operator TaruviBase doesn't support, such as Refine's eqs, fails
the request, including inside and/or groups. Array, range,
and full-text operators aren't in Refine's operator type; wrap those filters in
toRefineFilters():
import {toRefineFilters} from '@taruvi/refine-providers';
useList({
resource: 'tasks',
filters: toRefineFilters([{field: 'tags', operator: 'acontains', value: ['launch']}]),
});
Work with files#
Point a resource at a bucket with storageDataProvider. List rows are storage
objects: their id is a number, and file_path is the object path. Pass the
path, not the id, to useOne, useUpdate, and useDelete:
import {useCreate, useDelete, useList} from '@refinedev/core';
import type {HttpError} from '@refinedev/core';
import type {StorageUploadVariables} from '@taruvi/refine-providers';
import type {StorageObject} from '@taruvi/sdk';
export function Documents() {
const {result: {data: files}} = useList<StorageObject>({
resource: 'documents',
pagination: {currentPage: 1, pageSize: 50},
});
const {mutate: upload} = useCreate<StorageObject, HttpError, StorageUploadVariables>();
const {mutate: remove} = useDelete<StorageObject>();
return (
<>
<input
type="file"
onChange={(event) => {
const file = event.target.files?.[0];
if (file) upload({resource: 'documents', values: {files: [file], paths: [`uploads/${file.name}`]}});
}}
/>
<ul>
{files.map((file) => (
<li key={file.id}>
{file.filename}
<button type="button" onClick={() => remove({resource: 'documents', id: file.file_path})}>
Delete
</button>
</li>
))}
</ul>
</>
);
}
For a folder view, list with meta: {mode: 'browse', prefix: 'uploads/'}. Its
rows are folders and files; a file's object path is in path. A delete that
TaruviBase skips, because the object is missing or a policy denies it, fails
with the SDK's NotFoundError or ForbiddenError.
Show only what users may do#
accessControlProvider answers useCan, <CanAccess>, and Refine's action
buttons with your access policies. It
batches the checks on a page into one request.
Each check needs the policy's kind, which names the resource type and the resource:
| Resource | Kind | Actions |
|---|---|---|
| Data table | datatable:TABLE | read, create, update, delete |
| Storage bucket | storage:BUCKET | read, create, update, delete |
| Function | function:SLUG | execute |
| Saved query | query:SLUG | execute |
Refine's list and show are sent as read, edit as update, and clone
as create. The provider picks the kind in this order:
params.entityTypeon the check.meta.entityTypeon the Refine resource. Refine passes it from its action buttons and from<CanAccess>withoutparams, but not fromuseCan.- The
entityTypeoption, as inApp.tsxabove. It covers every check. - The resource name, which doesn't match a TaruviBase kind on its own.
import {CanAccess} from '@refinedev/core';
import type {ReactNode} from 'react';
export function IfCanDelete({taskId, children}: {taskId: string; children: ReactNode}) {
return (
<CanAccess resource="tasks" action="delete" params={{id: taskId}}>
{children}
</CanAccess>
);
}
The check sends params as the resource's attributes. A rule that depends on a
record's fields, such as "only the owner may delete", sees only what you pass.
For list pages, request meta.allowedActions instead: TaruviBase then decides
each row on the server and adds _allowed_actions to it.
Checks for your own TaruviBase Console account allow every action, so buttons never hide for you. Sign in as a site user with the role you want to test.
useCan and <CanAccess> only decide what to render. TaruviBase enforces your
policies on every request, whether or not the provider is registered.
Handle errors#
Provider calls reject with the SDK's error classes, and Refine hooks surface
them in their error state. When a request fails with 401, authProvider
sends the user to sign in again. A 403 leaves the user signed in: they lack
permission for that action. The
JavaScript SDK error reference lists
the classes.