XYLEX Group
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