DevelopmentResource Framework
ResourceProvider
ResourceProvider
The ResourceProvider is a centralized data management solution that provides real-time access to user resources with DMS event streaming integration.
Overview
The ResourceProvider manages four core resource types:
- User Preferences: User-specific settings and view configurations
- User Permission Scopes: User permission levels and access control
- Notifications: Company-wide notifications
- Feature Flags: User-specific feature toggles
Features
Real-Time Updates via DMS Events
The provider automatically listens to DMS events and updates state in real-time without refetching. When a resource changes in the database, the DMS sends an event with the complete data in the body, eliminating the need for additional API calls.
Automatic State Management
- Parallel fetching of all resources on initial mount
- Smart filtering based on user_id and company_id
- Deduplication of items on INSERT events
- Automatic WebSocket reconnection
Performance Optimized
- No unnecessary refetches - uses event body data directly
- Efficient state updates with React hooks
- Optional cache control
- Minimal re-renders with useMemo and useCallback
Installation
The ResourceProvider is exported from the resource framework:
import { ResourceProvider } from "@/packages/resource-framework";
```typescript
## Basic Usage
### Setup
Wrap your application with the ResourceProvider at a high level (e.g., root layout):
```tsx
import { ResourceProvider } from "@/packages/resource-framework";
export default function RootLayout({ children }) {
return (
<html>
<body>
<ResourceProvider cacheEnabled={true}>
{children}
</ResourceProvider>
</body>
</html>
);
}
```typescript
### Accessing Resources
Use the `useResourceContext` hook to access resources in any component:
```tsx
import { useResourceContext } from "@/packages/resource-framework";
function MyComponent() {
const {
userPreferences,
userScopes,
notifications,
flags,
hasScope,
hasFlag,
isLoading,
refetch
} = useResourceContext();
// Check permissions
if (hasScope("admin")) {
return <AdminDashboard />;
}
// Check feature flags
if (hasFlag("new_feature")) {
return <NewFeatureComponent />;
}
// Display notifications
const unreadCount = notifications.filter(n => !n.read).length;
return (
<div>
<NotificationBell count={unreadCount} />
{/* Rest of component */}
</div>
);
}
```typescript
## API Reference
### ResourceProvider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `React.ReactNode` | required | Your application components |
| `cacheEnabled` | `boolean` | `true` | Enable/disable cache control headers |
### useResourceContext Return Value
```typescript
interface ResourceContextValue {
// Resource arrays
userPreferences: UserPreference[];
userScopes: string[];
notifications: Notification[];
flags: string[];
// Helper functions
hasScope: (scope: string | string[], opts?: { all?: boolean }) => boolean;
hasFlag: (flag: string) => boolean;
// State and actions
isLoading: boolean;
refetch: () => Promise<void>;
}
```typescript
## Helper Functions
### hasScope
Check if the user has specific permission scopes:
```tsx
const { hasScope } = useResourceContext();
// Single scope
if (hasScope("admin")) {
// User has admin scope
}
// Multiple scopes (OR logic - user has ANY of these)
if (hasScope(["admin", "editor"])) {
// User has admin OR editor scope
}
// Multiple scopes (AND logic - user has ALL of these)
if (hasScope(["read", "write"], { all: true })) {
// User has both read AND write scopes
}
```typescript
### hasFlag
Check if a feature flag is enabled for the user:
```tsx
const { hasFlag } = useResourceContext();
if (hasFlag("beta_features")) {
return <BetaFeatureComponent />;
}
if (hasFlag("dark_mode_v2")) {
return <DarkModeToggle />;
}
```typescript
## Real-Time Event Handling
The ResourceProvider automatically handles DMS events for all resources:
### Event Structure
```typescript
// DMS Event Format
{
data: {
event: "INSERT" | "UPDATE" | "DELETE",
resource: "table_name",
body: { ...complete_record_data }
}
}
```typescript
### User Preferences Events
**INSERT**: Adds new preference if not already exists
```typescript
// Event body contains full preference data
{
id: 123,
user_id: "user-123",
table_name: "invoices",
settings: { columns: [...], rowsPerPage: 50 }
}
```typescript
**UPDATE**: Merges updated preference data
```typescript
// Event body contains updated fields
{
id: 123,
settings: { columns: [...], rowsPerPage: 100 }
}
```typescript
**DELETE**: Removes preference from state
```typescript
// Event body contains identifier
{
id: 123
}
```typescript
### User Permission Scopes Events
**INSERT**: Adds scope if enabled and matches user/company
```typescript
{
user_id: "user-123",
scope: "admin",
enabled: true,
company_id: "company-abc",
global: false
}
```typescript
**UPDATE**: Adds/removes scope based on enabled status
```typescript
{
user_id: "user-123",
scope: "admin",
enabled: false // Will remove from active scopes
}
```typescript
**DELETE**: Removes scope from active list
### Notifications Events
**INSERT**: Prepends new notification (filtered by company_id)
```typescript
{
notification_id: "notif-123",
company_id: "company-abc",
title: "New Invoice",
message: "Invoice #123 was created",
read: false,
created_at: "2024-01-01T00:00:00Z"
}
```typescript
**UPDATE**: Updates notification properties
```typescript
{
notification_id: "notif-123",
read: true // Mark as read
}
```typescript
**DELETE**: Removes notification from list
### Feature Flags Events
**UPDATE**: Updates flags array when user data changes
```typescript
// When user table or v_flags view updates
{
user_id: "user-123",
flags: ["beta_features", "dark_mode_v2"]
}
```typescript
## Advanced Usage
### Conditional Rendering Based on Scopes
```tsx
function AdminPanel() {
const { hasScope, isLoading } = useResourceContext();
if (isLoading) {
return <Skeleton />;
}
if (!hasScope("admin")) {
return <AccessDenied />;
}
return <AdminDashboard />;
}
```typescript
### Feature Gates
```tsx
function FeatureGate({ flag, children, fallback = null }) {
const { hasFlag, isLoading } = useResourceContext();
if (isLoading) return <Skeleton />;
if (!hasFlag(flag)) return fallback;
return <>{children}</>;
}
// Usage
<FeatureGate flag="new_dashboard">
<NewDashboard />
</FeatureGate>
```typescript
### Notification Center
```tsx
function NotificationCenter() {
const { notifications } = useResourceContext();
const unread = notifications.filter(n => !n.read);
const read = notifications.filter(n => n.read);
return (
<div>
<h3>Unread ({unread.length})</h3>
{unread.map(notif => (
<NotificationItem key={notif.notification_id} {...notif} />
))}
<h3>Read ({read.length})</h3>
{read.map(notif => (
<NotificationItem key={notif.notification_id} {...notif} />
))}
</div>
);
}
```typescript
### Manual Refresh
In rare cases where you need to force a refresh:
```tsx
function RefreshButton() {
const { refetch, isLoading } = useResourceContext();
const handleRefresh = async () => {
await refetch();
toast.success("Resources refreshed");
};
return (
<Button onClick={handleRefresh} disabled={isLoading}>
Refresh Resources
</Button>
);
}
```typescript
## Type Definitions
### UserPreference
```typescript
interface UserPreference {
id?: number;
user_preference_id?: string;
user_id?: string;
table_name?: string;
settings?: Record<string, any>;
created_at?: string;
updated_at?: string;
}
```typescript
### UserPermissionScope
```typescript
interface UserPermissionScope {
user_id?: string;
scope?: string;
enabled?: boolean;
global?: boolean;
company_id?: string;
}
```typescript
### Notification
```typescript
interface Notification {
notification_id?: string;
id?: string;
company_id?: string;
read?: boolean;
title?: string;
message?: string;
created_at?: string;
[key: string]: any;
}
```typescript
## Best Practices
### 1. Place High in Component Tree
Place the ResourceProvider as high as possible in your component tree to ensure all components have access:
```tsx
// ✅ Good - at root level
<ResourceProvider>
<App />
</ResourceProvider>
// ❌ Bad - nested deep
<App>
<Layout>
<ResourceProvider>
<Content />
</ResourceProvider>
</Layout>
</App>
```typescript
### 2. Use Selectively
Only destructure what you need to minimize re-renders:
```tsx
// ✅ Good - only what you need
const { hasScope } = useResourceContext();
// ❌ Bad - destructuring everything
const {
userPreferences,
userScopes,
notifications,
flags,
hasScope,
hasFlag,
isLoading,
refetch
} = useResourceContext();
```typescript
### 3. Leverage Helper Functions
Use `hasScope` and `hasFlag` instead of checking arrays directly:
```tsx
// ✅ Good - using helper
if (hasScope("admin")) { }
// ❌ Bad - manual check
if (userScopes.includes("admin")) { }
```typescript
### 4. Handle Loading States
Always handle the loading state on initial mount:
```tsx
const { hasScope, isLoading } = useResourceContext();
if (isLoading) {
return <LoadingSkeleton />;
}
if (!hasScope("view_dashboard")) {
return <AccessDenied />;
}
return <Dashboard />;
```typescript
### 5. Avoid Unnecessary Refetches
Trust the DMS events - they contain all the data you need. Only use `refetch()` when:
- User explicitly requests a refresh
- You suspect data might be stale
- After a long period of inactivity
## Performance Considerations
### Initial Fetch
- All resources fetched in parallel on mount
- Uses Promise.all for optimal performance
- Respects cache control settings
### Event Updates
- O(1) for flag/scope checks (using Set internally)
- O(n) for array updates (notifications, preferences)
- Arrays are typically small (< 100 items)
### Memory
- Automatic deduplication prevents duplicate entries
- WebSocket cleanup on unmount prevents memory leaks
- Event handlers use stable references (useCallback)
## Troubleshooting
### Resources Not Loading
**Problem**: `isLoading` stays true forever
**Solutions**:
- Check that user is authenticated (user_id, company_id, organization_id set)
- Verify API endpoint is accessible
- Check browser console for errors
- Ensure headers are properly configured
### Events Not Received
**Problem**: Real-time updates not working
**Solutions**:
- Check WebSocket connection in browser DevTools (Network > WS)
- Verify `user.company_id` is set
- Check DMS configuration in APP_CONFIG
- Ensure DMS is running and accessible
### Stale Data
**Problem**: Data doesn't match database
**Solutions**:
- Call `refetch()` to force refresh
- Check that event resource names match table names
- Verify event body contains expected structure
- Check filtering logic (company_id, user_id)
### Permission Checks Failing
**Problem**: `hasScope()` returns false unexpectedly
**Solutions**:
- Check that scopes are enabled in database
- Verify company_id matches for non-global scopes
- Ensure user_id matches current user
- Check for typos in scope names
## Examples
### Complete Dashboard Implementation
```tsx
// app/layout.tsx
import { ResourceProvider } from "@/packages/resource-framework";
export default function RootLayout({ children }) {
return (
<html>
<body>
<ResourceProvider>
{children}
</ResourceProvider>
</body>
</html>
);
}
// components/Header.tsx
import { useResourceContext } from "@/packages/resource-framework";
export function Header() {
const { notifications, hasScope } = useResourceContext();
const unread = notifications.filter(n => !n.read).length;
return (
<header>
<Logo />
<Navigation />
<NotificationBell count={unread} />
{hasScope("admin") && <AdminMenu />}
<UserMenu />
</header>
);
}
// components/ProtectedRoute.tsx
import { useResourceContext } from "@/packages/resource-framework";
import { redirect } from "next/navigation";
export function ProtectedRoute({
children,
requiredScope
}: {
children: React.ReactNode;
requiredScope: string;
}) {
const { hasScope, isLoading } = useResourceContext();
if (isLoading) {
return <LoadingSpinner />;
}
if (!hasScope(requiredScope)) {
redirect("/access-denied");
}
return <>{children}</>;
}
// Usage
<ProtectedRoute requiredScope="admin">
<AdminDashboard />
</ProtectedRoute>
```typescript
## Related Documentation
- [Resource Routes](./resource-routes.md) - Configure resource table routes
- [ResourceStore](./resource-store.md) - Paginated table data management
- [Hooks](./hooks.md) - Available hooks in the resource framework
- [Components](./components.md) - UI components for resources