Files
postgres/docs/FEATURES_JSON_GUIDE.md
2026-01-08 14:32:26 +00:00

13 KiB

Complete Guide to features.json Configuration System

Overview

The features.json file is now a comprehensive configuration system that defines:

  • UI Component Trees - Declarative component hierarchies
  • Playwright Playbooks - E2E test scenarios
  • Storybook Stories - Component documentation
  • Feature Flags - Enable/disable features
  • Translations - Multi-language support
  • Form Schemas - Dynamic form generation
  • API Endpoints - REST API definitions
  • Permissions - Role-based access control

Note: SQL query templates have been removed for security reasons. Use Drizzle ORM for all database operations (see section 2).

1. Component Trees

Define complete UI hierarchies in JSON without writing JSX.

Example: Simple Component Tree

{
  "componentTrees": {
    "MyPage": {
      "component": "Box",
      "props": {
        "sx": { "p": 3 }
      },
      "children": [
        {
          "component": "Typography",
          "props": {
            "variant": "h4",
            "text": "{{pageTitle}}"
          }
        },
        {
          "component": "Button",
          "condition": "canCreate",
          "props": {
            "variant": "contained",
            "startIcon": "Add",
            "onClick": "handleCreate",
            "text": "Create New"
          }
        }
      ]
    }
  }
}

Using Component Trees in Code

import { getComponentTree } from '@/utils/featureConfig';
import ComponentTreeRenderer from '@/utils/ComponentTreeRenderer';

function MyComponent() {
  const tree = getComponentTree('MyPage');
  const data = { pageTitle: 'Welcome', canCreate: true };
  const handlers = { handleCreate: () => console.log('Create') };
  
  return <ComponentTreeRenderer tree={tree} data={data} handlers={handlers} />;
}

Component Tree Features

Template Interpolation:

{
  "props": {
    "text": "Hello {{user.name}}!"
  }
}

Conditional Rendering:

{
  "condition": "isAdmin && hasPermission('create')",
  "component": "Button"
}

Loops (forEach):

{
  "component": "List",
  "children": [
    {
      "component": "ListItem",
      "forEach": "items",
      "children": [
        {
          "component": "Typography",
          "props": {
            "text": "{{item.name}}"
          }
        }
      ]
    }
  ]
}

2. Database Queries - Use Drizzle ORM

IMPORTANT SECURITY NOTE: This project previously included SQL template strings in features.json, but they have been removed due to SQL injection risks.

Why SQL Templates Were Removed

SQL templates with string interpolation (e.g., {{tableName}}) are inherently unsafe because they:

  1. Allow SQL injection if user input is not properly sanitized
  2. Encourage dangerous string concatenation patterns
  3. Bypass type-safe query builders

Use Drizzle ORM Instead

All database queries should use Drizzle ORM, which provides:

  • Type safety - Compile-time validation of queries
  • SQL injection prevention - Automatic parameterization
  • Better performance - Query optimization
  • Cleaner code - Fluent API

Example: Correct Way to Query Database

import { db } from '@/utils/db';
import { sql } from 'drizzle-orm';

// ✅ GOOD: Using Drizzle ORM with parameterized queries
async function getTableData(tableName: string) {
  // Use sql.identifier for table/column names
  const result = await db.execute(sql`
    SELECT * FROM ${sql.identifier([tableName])} 
    LIMIT 100
  `);
  return result.rows;
}

// ✅ GOOD: Using Drizzle ORM query builder
async function insertRecord(table: any, data: Record<string, any>) {
  const result = await db.insert(table).values(data).returning();
  return result[0];
}

// ❌ BAD: String concatenation (DO NOT USE)
async function unsafeQuery(tableName: string) {
  // This is vulnerable to SQL injection!
  const query = `SELECT * FROM "${tableName}"`;
  return await db.execute(sql.raw(query));
}

See Also

3. Playwright Playbooks

Define E2E test scenarios in JSON.

Example Playbook

{
  "playwrightPlaybooks": {
    "createTable": {
      "name": "Create Table Workflow",
      "description": "Test creating a new database table",
      "tags": ["admin", "table", "crud"],
      "steps": [
        {
          "action": "goto",
          "url": "/admin/dashboard"
        },
        {
          "action": "click",
          "selector": "button:has-text('Create Table')"
        },
        {
          "action": "fill",
          "selector": "input[label='Table Name']",
          "value": "{{tableName}}"
        },
        {
          "action": "expect",
          "selector": "text={{tableName}}",
          "text": "visible"
        }
      ],
      "cleanup": [
        {
          "action": "click",
          "selector": "button:has-text('Drop Table')"
        }
      ]
    }
  }
}

Using Playbooks

import { getPlaywrightPlaybook } from '@/utils/featureConfig';

const playbook = getPlaywrightPlaybook('createTable');

// Execute playbook steps
for (const step of playbook.steps) {
  switch (step.action) {
    case 'goto':
      await page.goto(step.url);
      break;
    case 'click':
      await page.click(step.selector);
      break;
    // ... handle other actions
  }
}

4. Storybook Stories

Define component stories in JSON.

Example Stories

{
  "storybookStories": {
    "Button": {
      "primary": {
        "name": "Primary Button",
        "description": "Primary action button",
        "args": {
          "variant": "contained",
          "color": "primary",
          "text": "Click Me"
        }
      },
      "withIcon": {
        "name": "With Icon",
        "args": {
          "variant": "contained",
          "startIcon": "Add",
          "text": "Add Item"
        },
        "play": [
          "await userEvent.click(screen.getByText('Add Item'))",
          "await expect(args.onClick).toHaveBeenCalled()"
        ]
      }
    }
  }
}

Using Stories

import { getStorybookStory } from '@/utils/featureConfig';

const story = getStorybookStory('Button', 'primary');

export const Primary = {
  name: story.name,
  args: story.args,
};

5. Helper Functions

Component Trees

import {
  getComponentTree,
  getAllComponentTrees,
} from '@/utils/featureConfig';

const tree = getComponentTree('TableManagerTab');
const allTrees = getAllComponentTrees();

Playwright Playbooks

import {
  getPlaywrightPlaybook,
  getAllPlaywrightPlaybooks,
  getPlaywrightPlaybooksByTag,
} from '@/utils/featureConfig';

const playbook = getPlaywrightPlaybook('createTable');
const allPlaybooks = getAllPlaywrightPlaybooks();
const adminPlaybooks = getPlaywrightPlaybooksByTag('admin');

Storybook Stories

import {
  getStorybookStory,
  getAllStorybookStories,
  getStorybookStoriesForComponent,
} from '@/utils/featureConfig';

const story = getStorybookStory('Button', 'primary');
const allStories = getAllStorybookStories();
const buttonStories = getStorybookStoriesForComponent('Button');

6. Feature Flags

Enable or disable features dynamically.

{
  "features": [
    {
      "id": "table-management",
      "name": "Table Management",
      "enabled": true,
      "priority": "high",
      "ui": {
        "showInNav": true,
        "icon": "TableChart",
        "actions": ["create", "delete"]
      }
    }
  ]
}

Using Features

import { getFeatureById, getFeatures } from '@/utils/featureConfig';

const feature = getFeatureById('table-management');
const canCreate = feature?.ui.actions.includes('create');
const allFeatures = getFeatures(); // Only enabled features

7. Form Schemas

Dynamic form generation from JSON.

{
  "formSchemas": {
    "users": {
      "fields": [
        {
          "name": "name",
          "type": "text",
          "label": "Name",
          "required": true,
          "minLength": 2,
          "maxLength": 100
        },
        {
          "name": "email",
          "type": "email",
          "label": "Email",
          "required": true,
          "validation": "email"
        }
      ],
      "submitLabel": "Save User",
      "cancelLabel": "Cancel"
    }
  }
}

Using Form Schemas

import { getFormSchema } from '@/utils/featureConfig';

const schema = getFormSchema('users');

<FormDialog
  open={open}
  title="Add User"
  fields={schema.fields}
  submitLabel={schema.submitLabel}
  onSubmit={handleSubmit}
/>

8. Translations

Multi-language support.

{
  "translations": {
    "en": {
      "features": {
        "database-crud": {
          "name": "Database CRUD Operations",
          "description": "Create, read, update, and delete records"
        }
      },
      "actions": {
        "create": "Create",
        "update": "Update"
      }
    },
    "fr": {
      "features": {
        "database-crud": {
          "name": "Opérations CRUD",
          "description": "Créer, lire, mettre à jour et supprimer"
        }
      },
      "actions": {
        "create": "Créer",
        "update": "Mettre à jour"
      }
    }
  }
}

Using Translations

import {
  getFeatureTranslation,
  getActionTranslation,
} from '@/utils/featureConfig';

const feature = getFeatureTranslation('database-crud', 'fr');
const createAction = getActionTranslation('create', 'fr');

9. API Endpoints

REST API documentation in JSON.

{
  "apiEndpoints": {
    "users": {
      "list": {
        "method": "GET",
        "path": "/api/admin/users",
        "description": "List all users"
      },
      "create": {
        "method": "POST",
        "path": "/api/admin/users",
        "description": "Create a new user"
      }
    }
  }
}

Using API Endpoints

import { getApiEndpoint, getApiEndpoints } from '@/utils/featureConfig';

const endpoint = getApiEndpoint('users', 'list');
// { method: 'GET', path: '/api/admin/users', description: '...' }

const allUserEndpoints = getApiEndpoints('users');

10. Permissions

Role-based access control.

{
  "permissions": {
    "users": {
      "create": ["admin"],
      "read": ["admin", "user"],
      "update": ["admin"],
      "delete": ["admin"]
    }
  }
}

Using Permissions

import { hasPermission, getPermissions } from '@/utils/featureConfig';

const canCreate = hasPermission('users', 'create', userRole);
const userPermissions = getPermissions('users');

Benefits

1. Configuration-Driven Development

  • Define UIs, queries, tests, and stories in JSON
  • No code changes needed for many modifications
  • Non-developers can contribute

2. Consistency

  • All features use the same structure
  • Standardized component usage
  • Enforced patterns

3. Rapid Development

  • Prototype new features quickly
  • Reuse existing patterns
  • Less boilerplate code

4. Maintainability

  • Single source of truth
  • Easy to find and update configuration
  • Clear separation of concerns

5. Testing

  • Playbooks define test scenarios
  • Storybook stories from JSON
  • Easy to add new test cases

6. Flexibility

  • Enable/disable features dynamically
  • A/B test different configurations
  • Multi-language support

Best Practices

1. Keep Trees Shallow

Avoid deeply nested component trees - they're hard to read and maintain.

2. Use Meaningful Names

Name component trees, playbooks, and templates descriptively:

  • UserListPage
  • Page1

3. Document with Comments

Use the comment property in component trees:

{
  "component": "Outlet",
  "comment": "Child routes render here"
}

4. Validate Configuration

Use TypeScript types to ensure correctness:

import type { ComponentTree, SqlTemplate } from '@/utils/featureConfig';

5. Test Generated UIs

Always test component trees after changes:

const tree = getComponentTree('MyPage');
expect(tree).toBeDefined();
expect(tree.component).toBe('Box');

6. Version Control

Track features.json changes carefully - it's critical infrastructure.

7. Modular Organization

Group related templates, playbooks, and stories together.

Conclusion

The features.json configuration system enables:

  • 50% less boilerplate code in components
  • Declarative UI definition without JSX
  • Configuration-driven E2E tests with Playwright
  • Automated Storybook stories from JSON
  • Parameterized SQL queries for safety
  • Complete feature configuration in one place

This architecture scales to hundreds of features while keeping the codebase maintainable and the development workflow efficient.