Redesign sqlTemplates with secure Drizzle ORM structure

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-08 14:32:26 +00:00
parent 3ce0573a3b
commit 4c2bc9e09c
3 changed files with 51 additions and 273 deletions

View File

@@ -4,7 +4,6 @@
The `features.json` file is now a comprehensive configuration system that defines:
-**UI Component Trees** - Declarative component hierarchies
-**SQL Query Templates** - Parameterized database queries
-**Playwright Playbooks** - E2E test scenarios
-**Storybook Stories** - Component documentation
-**Feature Flags** - Enable/disable features
@@ -13,6 +12,8 @@ The `features.json` file is now a comprehensive configuration system that define
-**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.
@@ -104,53 +105,59 @@ function MyComponent() {
}
```
## 2. SQL Templates
## 2. Database Queries - Use Drizzle ORM
Parameterized SQL queries with template variables.
**IMPORTANT SECURITY NOTE:** This project previously included SQL template strings in `features.json`, but they have been removed due to SQL injection risks.
### Example SQL Templates
```json
{
"sqlTemplates": {
"tables": {
"createTable": {
"description": "Create a new table with columns",
"query": "CREATE TABLE \"{{tableName}}\" ({{columnDefinitions}})",
"returns": "command"
},
"listTables": {
"description": "Get all tables",
"query": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'",
"returns": "rows"
}
},
"records": {
"insert": {
"description": "Insert a new record",
"query": "INSERT INTO \"{{tableName}}\" ({{columns}}) VALUES ({{values}}) RETURNING *",
"returns": "rows"
}
}
}
### 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
```typescript
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));
}
```
### Using SQL Templates
```typescript
import { getSqlTemplate, interpolateSqlTemplate } from '@/utils/featureConfig';
// Get template
const template = getSqlTemplate('records', 'insert');
// Interpolate parameters
const query = interpolateSqlTemplate(template, {
tableName: 'users',
columns: 'name, email',
values: '$1, $2'
});
// Result: INSERT INTO "users" (name, email) VALUES ($1, $2) RETURNING *
```
### See Also
- [Drizzle ORM Documentation](https://orm.drizzle.team/docs/overview)
- [SQL Injection Prevention Guide](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)
- API route examples in `src/app/api/admin/` directory
## 3. Playwright Playbooks
@@ -275,21 +282,6 @@ const tree = getComponentTree('TableManagerTab');
const allTrees = getAllComponentTrees();
```
### SQL Templates
```typescript
import {
getSqlTemplate,
getAllSqlTemplates,
getSqlTemplatesByCategory,
interpolateSqlTemplate,
} from '@/utils/featureConfig';
const template = getSqlTemplate('records', 'insert');
const allTemplates = getAllSqlTemplates();
const recordTemplates = getSqlTemplatesByCategory('records');
const query = interpolateSqlTemplate(template, { tableName: 'users' });
```
### Playwright Playbooks
```typescript
import {

View File

@@ -2895,185 +2895,6 @@
{ "value": "IS NULL", "label": "Is Null" },
{ "value": "IS NOT NULL", "label": "Is Not Null" }
],
"sqlTemplates": {
"validation": {
"validateTable": {
"description": "Check if a table exists in the public schema",
"query": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = {{tableName}}",
"returns": "rows"
},
"validateColumn": {
"description": "Check if a column exists in a table",
"query": "SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = {{tableName}} AND column_name = {{columnName}}",
"returns": "rows"
}
},
"tables": {
"listTables": {
"description": "Get all tables in the public schema",
"query": "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name",
"returns": "rows"
},
"getTableSchema": {
"description": "Get detailed schema information for a table",
"query": "SELECT column_name, data_type, is_nullable, column_default, character_maximum_length FROM information_schema.columns WHERE table_schema = 'public' AND table_name = {{tableName}} ORDER BY ordinal_position",
"returns": "rows"
},
"getTablePrimaryKeys": {
"description": "Get primary key columns for a table",
"query": "SELECT kcu.column_name FROM information_schema.table_constraints tc JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public' AND tc.table_name = {{tableName}}",
"returns": "rows"
},
"getTableData": {
"description": "Get all data from a table with limit",
"query": "SELECT * FROM \"{{tableName}}\" LIMIT {{limit}}",
"returns": "rows",
"defaultParams": {
"limit": 100
}
},
"createTable": {
"description": "Create a new table with columns",
"query": "CREATE TABLE \"{{tableName}}\" ({{columnDefinitions}})",
"returns": "command",
"example": "CREATE TABLE \"users\" (id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE)"
},
"dropTable": {
"description": "Drop a table",
"query": "DROP TABLE IF EXISTS \"{{tableName}}\" CASCADE",
"returns": "command"
}
},
"columns": {
"addColumn": {
"description": "Add a new column to a table",
"query": "ALTER TABLE \"{{tableName}}\" ADD COLUMN \"{{columnName}}\" {{dataType}}{{nullable}}{{defaultValue}}",
"returns": "command",
"example": "ALTER TABLE \"users\" ADD COLUMN \"age\" INTEGER NOT NULL DEFAULT 0"
},
"modifyColumn": {
"description": "Modify column data type",
"query": "ALTER TABLE \"{{tableName}}\" ALTER COLUMN \"{{columnName}}\" TYPE {{dataType}}",
"returns": "command"
},
"renameColumn": {
"description": "Rename a column",
"query": "ALTER TABLE \"{{tableName}}\" RENAME COLUMN \"{{oldName}}\" TO \"{{newName}}\"",
"returns": "command"
},
"dropColumn": {
"description": "Drop a column from a table",
"query": "ALTER TABLE \"{{tableName}}\" DROP COLUMN \"{{columnName}}\"",
"returns": "command"
},
"setColumnNull": {
"description": "Set a column to allow NULL values",
"query": "ALTER TABLE \"{{tableName}}\" ALTER COLUMN \"{{columnName}}\" DROP NOT NULL",
"returns": "command"
},
"setColumnNotNull": {
"description": "Set a column to NOT NULL",
"query": "ALTER TABLE \"{{tableName}}\" ALTER COLUMN \"{{columnName}}\" SET NOT NULL",
"returns": "command"
}
},
"records": {
"insert": {
"description": "Insert a new record into a table",
"query": "INSERT INTO \"{{tableName}}\" ({{columns}}) VALUES ({{values}}) RETURNING *",
"returns": "rows",
"example": "INSERT INTO \"users\" (name, email) VALUES ('John Doe', 'john@example.com') RETURNING *"
},
"update": {
"description": "Update a record in a table",
"query": "UPDATE \"{{tableName}}\" SET {{setClause}} WHERE {{whereClause}} RETURNING *",
"returns": "rows",
"example": "UPDATE \"users\" SET name = 'Jane Doe' WHERE id = 1 RETURNING *"
},
"delete": {
"description": "Delete a record from a table",
"query": "DELETE FROM \"{{tableName}}\" WHERE {{whereClause}} RETURNING *",
"returns": "rows",
"example": "DELETE FROM \"users\" WHERE id = 1 RETURNING *"
},
"select": {
"description": "Select records from a table with conditions",
"query": "SELECT {{columns}} FROM \"{{tableName}}\"{{whereClause}}{{orderBy}}{{limit}}",
"returns": "rows",
"example": "SELECT id, name, email FROM \"users\" WHERE active = true ORDER BY created_at DESC LIMIT 50"
}
},
"constraints": {
"listConstraints": {
"description": "List all constraints for a table",
"query": "SELECT tc.constraint_name, tc.constraint_type, STRING_AGG(kcu.column_name, ', ') as columns, cc.check_clause FROM information_schema.table_constraints tc LEFT JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema LEFT JOIN information_schema.check_constraints cc ON tc.constraint_name = cc.constraint_name WHERE tc.table_schema = 'public' AND tc.table_name = {{tableName}} GROUP BY tc.constraint_name, tc.constraint_type, cc.check_clause",
"returns": "rows"
},
"addPrimaryKey": {
"description": "Add primary key constraint",
"query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" PRIMARY KEY ({{columns}})",
"returns": "command"
},
"addUnique": {
"description": "Add unique constraint",
"query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" UNIQUE ({{columns}})",
"returns": "command"
},
"addCheck": {
"description": "Add check constraint",
"query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" CHECK ({{expression}})",
"returns": "command",
"example": "ALTER TABLE \"users\" ADD CONSTRAINT \"age_positive\" CHECK (age >= 0)"
},
"addForeignKey": {
"description": "Add foreign key constraint",
"query": "ALTER TABLE \"{{tableName}}\" ADD CONSTRAINT \"{{constraintName}}\" FOREIGN KEY ({{columns}}) REFERENCES \"{{refTable}}\" ({{refColumns}})",
"returns": "command"
},
"dropConstraint": {
"description": "Drop a constraint",
"query": "ALTER TABLE \"{{tableName}}\" DROP CONSTRAINT \"{{constraintName}}\"",
"returns": "command"
}
},
"indexes": {
"listIndexes": {
"description": "List all indexes for a table",
"query": "SELECT i.relname as indexname, ix.indisprimary as is_primary, ix.indisunique as is_unique, am.amname as index_type, pg_get_indexdef(ix.indexrelid) as indexdef FROM pg_class t JOIN pg_index ix ON t.oid = ix.indrelid JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_am am ON i.relam = am.oid WHERE t.relkind = 'r' AND t.relname = {{tableName}} AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'public') ORDER BY i.relname",
"returns": "rows"
},
"createIndex": {
"description": "Create an index on a table",
"query": "CREATE{{unique}} INDEX{{ifNotExists}} \"{{indexName}}\" ON \"{{tableName}}\" USING {{indexType}} ({{columns}})",
"returns": "command",
"example": "CREATE INDEX \"idx_users_email\" ON \"users\" USING BTREE (email)",
"defaultParams": {
"indexType": "BTREE",
"unique": "",
"ifNotExists": ""
}
},
"dropIndex": {
"description": "Drop an index",
"query": "DROP INDEX IF EXISTS \"{{indexName}}\"",
"returns": "command"
}
},
"queryBuilder": {
"buildSelect": {
"description": "Build a SELECT query with filters, sorting, and pagination",
"query": "SELECT {{columns}} FROM \"{{tableName}}\"{{where}}{{orderBy}}{{limit}}{{offset}}",
"returns": "rows",
"defaultParams": {
"columns": "*",
"where": "",
"orderBy": "",
"limit": "",
"offset": ""
}
}
}
},
"playwrightPlaybooks": {
"adminLogin": {
"name": "Admin Login Flow",

View File

@@ -196,13 +196,7 @@ export type ComponentPropSchema = {
props: Record<string, PropDefinition>;
};
export type SqlTemplate = {
description: string;
query: string;
returns: 'rows' | 'command';
example?: string;
defaultParams?: Record<string, any>;
};
export type PlaywrightStep = {
action: 'goto' | 'click' | 'fill' | 'select' | 'wait' | 'expect' | 'screenshot';
@@ -248,7 +242,6 @@ type FeaturesConfig = {
uiViews?: Record<string, Record<string, UiView>>;
componentTrees?: Record<string, ComponentTree>;
componentProps?: Record<string, ComponentPropSchema>;
sqlTemplates?: Record<string, Record<string, SqlTemplate>>;
playwrightPlaybooks?: Record<string, PlaywrightPlaybook>;
storybookStories?: Record<string, Record<string, StorybookStory>>;
features: Feature[];
@@ -448,34 +441,6 @@ export function getComponentsByCategory(category: string): string[] {
.map(([name, _]) => name);
}
// SQL Templates
export function getSqlTemplate(category: string, templateName: string): SqlTemplate | undefined {
return config.sqlTemplates?.[category]?.[templateName];
}
export function getAllSqlTemplates(): Record<string, Record<string, SqlTemplate>> {
return config.sqlTemplates || {};
}
export function getSqlTemplatesByCategory(category: string): Record<string, SqlTemplate> {
return config.sqlTemplates?.[category] || {};
}
export function interpolateSqlTemplate(template: SqlTemplate, params: Record<string, any>): string {
let query = template.query;
// Merge default params with provided params
const allParams = { ...template.defaultParams, ...params };
// Replace template variables
Object.entries(allParams).forEach(([key, value]) => {
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
query = query.replace(regex, String(value));
});
return query;
}
// Playwright Playbooks
export function getPlaywrightPlaybook(playbookName: string): PlaywrightPlaybook | undefined {
return config.playwrightPlaybooks?.[playbookName];