api-v1/db/queryHelper/DEVELOPER_GUIDE.md

1165 lines
33 KiB
Markdown

# Query Helper - Developer Guide
Complete guide for developers on how to use the JSON-based database query helper in this codebase.
## 📚 Table of Contents
1. [Introduction](#introduction)
2. [Getting Started](#getting-started)
3. [Core Functions](#core-functions)
4. [Real-World Examples](#real-world-examples)
5. [Common Patterns](#common-patterns)
6. [Flow & Architecture](#flow--architecture)
7. [Best Practices](#best-practices)
8. [Troubleshooting](#troubleshooting)
---
## Introduction
The Query Helper is a centralized, SQL-string-free database interface that uses JSON objects to build safe, parameterized queries. It eliminates the need for raw SQL strings and provides a consistent, maintainable way to interact with the database.
### Why Use Query Helper?
**No SQL Strings** - All queries built using JSON objects
**SQL Injection Safe** - All queries are parameterized
**Type Safe** - Structured JSON prevents errors
**Consistent** - Same pattern across all routes
**Maintainable** - Easy to read and modify
**Testable** - Easy to mock and test
---
## Getting Started
### 1. Import the Functions
```javascript
import { select, insert, update, deleteRecord, execute } from '../db/queryHelper/index.js';
```
### 2. Basic Usage Pattern
```javascript
// SELECT example
const users = await select({
table: 'users',
where: { deleted: false },
limit: 10
});
// INSERT example
const user = await insert({
table: 'users',
data: { name: 'John', phone_number: '+1234567890' },
returning: '*'
});
```
---
## Core Functions
### 1. `select(options)` - Retrieve Records
**Purpose:** Fetch records from the database with filtering, sorting, and pagination.
**Options:**
- `table` (string, **required**): Table name
- `columns` (string[] | '*', optional): Columns to select (default: '*')
- `where` (object, optional): WHERE conditions
- `orderBy` (object, optional): `{ column: string, direction: 'asc'|'desc' }`
- `limit` (number, optional): Max records (capped at 100)
- `offset` (number, optional): Skip records
- `joins` (array, optional): Join configurations
**Returns:** `Promise<object[]>` - Array of records
**Example from codebase:**
```javascript
// From routes/userRoutes.js - GET All Users
router.get("/", async (req, res) => {
try {
const { limit = 100, offset = 0 } = req.query;
const users = await select({
table: 'users',
columns: ['id', 'name', 'phone_number', 'avatar_url', 'language', 'timezone', 'country_code', 'is_active', 'created_at', 'updated_at'],
where: {
deleted: false,
},
orderBy: {
column: 'created_at',
direction: 'desc',
},
limit: Math.min(limit, 100),
offset: offset,
});
res.json(users);
} catch (err) {
console.error("Error fetching users:", err);
res.status(500).json({ error: "Internal server error" });
}
});
```
### 2. `insert(options)` - Create Records
**Purpose:** Insert new record(s) into the database.
**Options:**
- `table` (string, **required**): Table name
- `data` (object | object[], **required**): Data to insert (single object or array for batch)
- `returning` (string[] | '*', optional): Columns to return (PostgreSQL)
**Returns:** `Promise<object | object[]>` - Inserted record(s)
**Example from codebase:**
```javascript
// From routes/userRoutes.js - CREATE User
router.post("/", async (req, res) => {
try {
const {
id, // Optional: if provided, use this UUID; otherwise generate one
name,
phone_number,
avatar_url,
language,
timezone,
country_code = "+91",
} = req.body;
// Build user data object
const userData = {
name,
phone_number,
avatar_url: avatar_url || null,
language: language || null,
timezone: timezone || null,
country_code,
};
// If id is provided, include it; otherwise let the database generate one
if (id) {
userData.id = id;
}
const user = await insert({
table: 'users',
data: userData,
returning: '*',
});
res.status(201).json(user);
} catch (err) {
console.error("Error creating user:", err);
// ... error handling
}
});
```
### 3. `update(options)` - Update Records
**Purpose:** Update existing records in the database.
**Options:**
- `table` (string, **required**): Table name
- `data` (object, **required**): Data to update
- `where` (object, **required**): WHERE conditions (required for safety)
- `returning` (string[] | '*', optional): Columns to return
**Returns:** `Promise<object[]>` - Updated records
**Example from codebase:**
```javascript
// From routes/userRoutes.js - UPDATE User
router.put("/:id", async (req, res) => {
try {
const { id } = req.params;
const { name, phone_number, avatar_url, language, timezone, country_code, is_active } = req.body;
// Build update data object (only include fields that are provided)
const updateData = {};
if (name !== undefined) updateData.name = name;
if (phone_number !== undefined) updateData.phone_number = phone_number;
if (avatar_url !== undefined) updateData.avatar_url = avatar_url;
if (language !== undefined) updateData.language = language;
if (timezone !== undefined) updateData.timezone = timezone;
if (country_code !== undefined) updateData.country_code = country_code;
if (is_active !== undefined) updateData.is_active = is_active;
const updated = await update({
table: 'users',
data: updateData,
where: {
id: id,
deleted: false,
},
returning: '*',
});
if (updated.length === 0) {
return res.status(404).json({ error: "User not found" });
}
res.json(updated[0]);
} catch (err) {
console.error("Error updating user:", err);
res.status(500).json({ error: "Internal server error" });
}
});
```
### 4. `deleteRecord(options)` - Delete Records
**Purpose:** Delete records from the database (hard delete).
**Options:**
- `table` (string, **required**): Table name
- `where` (object, **required**): WHERE conditions (required for safety)
- `returning` (string[] | '*', optional): Columns to return
**Returns:** `Promise<object[]>` - Deleted records
**Note:** For soft deletes, use `update()` instead (see example below).
**Example - Soft Delete (Recommended):**
```javascript
// From routes/userRoutes.js - DELETE User (Soft Delete)
router.delete("/:id", async (req, res) => {
try {
const { id } = req.params;
const deleted = await update({
table: 'users',
data: {
deleted: true,
},
where: {
id: id,
},
returning: ['id'],
});
if (deleted.length === 0) {
return res.status(404).json({ error: "User not found" });
}
res.json({ message: "User deleted successfully" });
} catch (err) {
console.error("Error deleting user:", err);
res.status(500).json({ error: "Internal server error" });
}
});
```
### 5. `execute(options)` - Transactions & Custom Queries
**Purpose:** Execute transactions or custom query builder logic.
**Options:**
- `type` (string, **required**): 'transaction' or 'raw-builder'
- `handler` (function, **required**): Handler function receiving knex/trx instance
**Returns:** `Promise<any>` - Result from handler function
**Example - Transaction:**
```javascript
// Creating a listing with animal in a transaction
const result = await execute({
type: 'transaction',
handler: async (trx) => {
// Insert animal
const animalResult = await trx('animals')
.insert({
species_id: animal.species_id,
breed_id: animal.breed_id,
sex: animal.sex,
// ... other fields
})
.returning('*');
const animal_id = animalResult[0].id;
// Insert listing
const listingResult = await trx('listings')
.insert({
seller_id: seller_id,
animal_id: animal_id,
title: title,
price: price,
// ... other fields
})
.returning('*');
return listingResult[0];
},
});
```
**Example - Custom Query Builder:**
```javascript
// Complex query with multiple conditions on same column
const listings = await execute({
type: 'raw-builder',
handler: async (knex) => {
let query = knex('listings')
.where('deleted', false)
.where('status', status);
if (price_min) {
query = query.where('price', '>=', price_min);
}
if (price_max) {
query = query.where('price', '<=', price_max);
}
return await query
.orderBy('created_at', 'desc')
.limit(limit)
.offset(offset);
},
});
```
---
## WHERE Clause Operators
### Simple Equality
```javascript
where: { status: 'active' }
// SQL: WHERE status = 'active'
```
### Comparison Operators
```javascript
where: {
price: { op: '>=', value: 1000 },
age: { op: '<', value: 65 }
}
// SQL: WHERE price >= 1000 AND age < 65
```
### IN Operator
```javascript
where: {
id: { op: 'in', value: ['uuid1', 'uuid2', 'uuid3'] }
}
// SQL: WHERE id IN ('uuid1', 'uuid2', 'uuid3')
```
### LIKE Operator
```javascript
where: {
name: { op: 'ilike', value: '%john%' }
}
// SQL: WHERE name ILIKE '%john%'
```
### BETWEEN Operator
```javascript
where: {
age: { op: 'between', value: [18, 65] }
}
// SQL: WHERE age BETWEEN 18 AND 65
```
### NULL Checks
```javascript
where: {
deleted_at: { op: 'isNull' }
}
// SQL: WHERE deleted_at IS NULL
```
### Supported Operators
| Operator | Description | Example |
|----------|-------------|---------|
| `>` | Greater than | `{ op: '>', value: 100 }` |
| `<` | Less than | `{ op: '<', value: 100 }` |
| `>=` | Greater than or equal | `{ op: '>=', value: 100 }` |
| `<=` | Less than or equal | `{ op: '<=', value: 100 }` |
| `!=` or `<>` | Not equal | `{ op: '!=', value: 'deleted' }` |
| `in` | In array | `{ op: 'in', value: [1, 2, 3] }` |
| `notIn` | Not in array | `{ op: 'notIn', value: [1, 2, 3] }` |
| `like` | Case-sensitive LIKE | `{ op: 'like', value: '%test%' }` |
| `ilike` | Case-insensitive LIKE | `{ op: 'ilike', value: '%test%' }` |
| `between` | Between two values | `{ op: 'between', value: [10, 20] }` |
| `isNull` | IS NULL | `{ op: 'isNull' }` |
| `isNotNull` | IS NOT NULL | `{ op: 'isNotNull' }` |
---
## Real-World Examples
### Example 1: GET Endpoint with Filters
```javascript
// GET /users - Get all active users with pagination
router.get("/", async (req, res) => {
try {
const { limit = 100, offset = 0, status } = req.query;
const whereConditions = {
deleted: false,
};
// Add optional status filter
if (status) {
whereConditions.status = status;
}
const users = await select({
table: 'users',
columns: ['id', 'name', 'phone_number', 'created_at'],
where: whereConditions,
orderBy: {
column: 'created_at',
direction: 'desc',
},
limit: Math.min(limit, 100), // Cap at 100
offset: parseInt(offset),
});
res.json(users);
} catch (err) {
console.error("Error fetching users:", err);
res.status(500).json({ error: "Internal server error" });
}
});
```
### Example 2: POST Endpoint with Validation
```javascript
// POST /users - Create a new user
router.post("/", async (req, res) => {
try {
const { name, phone_number, country_code = "+91" } = req.body;
// Validation
if (!name || !phone_number) {
return res.status(400).json({ error: "Name and phone_number are required" });
}
// Build data object
const userData = {
name,
phone_number,
country_code,
};
// Insert with returning all fields
const user = await insert({
table: 'users',
data: userData,
returning: '*',
});
res.status(201).json(user);
} catch (err) {
console.error("Error creating user:", err);
// Handle unique constraint violation
if (err.code === "23505") {
return res.status(400).json({
error: "A user with this phone number already exists",
});
}
res.status(500).json({ error: "Internal server error" });
}
});
```
### Example 3: PUT Endpoint with Partial Updates
```javascript
// PUT /users/:id - Update user (partial update)
router.put("/:id", async (req, res) => {
try {
const { id } = req.params;
const { name, phone_number, avatar_url } = req.body;
// Build update data (only include provided fields)
const updateData = {};
if (name !== undefined) updateData.name = name;
if (phone_number !== undefined) updateData.phone_number = phone_number;
if (avatar_url !== undefined) updateData.avatar_url = avatar_url;
// Check if any fields to update
if (Object.keys(updateData).length === 0) {
return res.status(400).json({ error: "No fields to update" });
}
const updated = await update({
table: 'users',
data: updateData,
where: {
id: id,
deleted: false, // Ensure not updating deleted users
},
returning: '*',
});
if (updated.length === 0) {
return res.status(404).json({ error: "User not found" });
}
res.json(updated[0]);
} catch (err) {
console.error("Error updating user:", err);
res.status(500).json({ error: "Internal server error" });
}
});
```
### Example 4: Complex Query with Multiple Filters
```javascript
// GET /listings - Get listings with multiple filters
router.get("/", async (req, res) => {
try {
const {
status = "active",
species_id,
price_min,
price_max,
limit = 20,
offset = 0
} = req.query;
// For complex queries with multiple conditions on same column, use execute()
const listings = await execute({
type: 'raw-builder',
handler: async (knex) => {
let query = knex('listings')
.where('deleted', false)
.where('status', status);
if (species_id) {
query = query.where('filter_species_id', species_id);
}
if (price_min) {
query = query.where('price', '>=', price_min);
}
if (price_max) {
query = query.where('price', '<=', price_max);
}
return await query
.orderBy('created_at', 'desc')
.limit(Math.min(limit, 100))
.offset(offset);
},
});
res.json(listings);
} catch (err) {
console.error("Error fetching listings:", err);
res.status(500).json({ error: "Internal server error" });
}
});
```
### Example 5: Transaction with Multiple Operations
```javascript
// POST /listings - Create listing with animal and media
router.post("/", async (req, res) => {
try {
const { seller_id, title, price, animal, media } = req.body;
const result = await execute({
type: 'transaction',
handler: async (trx) => {
// 1. Insert animal
const animalResult = await trx('animals')
.insert({
species_id: animal.species_id,
breed_id: animal.breed_id,
sex: animal.sex,
age_months: animal.age_months,
// ... other fields
})
.returning('*');
const animal_id = animalResult[0].id;
// 2. Insert listing
const listingResult = await trx('listings')
.insert({
seller_id: seller_id,
animal_id: animal_id,
title: title,
price: price,
status: 'active',
})
.returning('*');
const listing_id = listingResult[0].id;
// 3. Insert media (batch)
if (media && media.length > 0) {
const mediaData = media.map(item => ({
listing_id: listing_id,
media_url: item.media_url,
media_type: item.media_type,
is_primary: item.is_primary || false,
sort_order: item.sort_order || 0,
}));
await trx('listing_media').insert(mediaData);
}
return listingResult[0];
},
});
res.status(201).json(result);
} catch (err) {
console.error("Error creating listing:", err);
res.status(500).json({ error: "Internal server error" });
}
});
```
---
## Common Patterns
### Pattern 1: GET with Pagination
```javascript
const { limit = 20, offset = 0 } = req.query;
const results = await select({
table: 'table_name',
where: { deleted: false },
orderBy: { column: 'created_at', direction: 'desc' },
limit: Math.min(limit, 100), // Always cap limit
offset: parseInt(offset),
});
```
### Pattern 2: GET Single Record by ID
```javascript
const { id } = req.params;
const records = await select({
table: 'table_name',
where: {
id: id,
deleted: false, // Always check deleted flag
},
limit: 1,
});
if (records.length === 0) {
return res.status(404).json({ error: "Record not found" });
}
res.json(records[0]);
```
### Pattern 3: Partial Update (PATCH-style)
```javascript
const { id } = req.params;
const updateData = {};
// Only include fields that are provided
if (req.body.name !== undefined) updateData.name = req.body.name;
if (req.body.email !== undefined) updateData.email = req.body.email;
if (Object.keys(updateData).length === 0) {
return res.status(400).json({ error: "No fields to update" });
}
const updated = await update({
table: 'table_name',
data: updateData,
where: { id: id },
returning: '*',
});
```
### Pattern 4: Soft Delete
```javascript
// Always use UPDATE for soft deletes, not deleteRecord()
const deleted = await update({
table: 'table_name',
data: { deleted: true },
where: { id: id },
returning: ['id'],
});
```
### Pattern 5: Batch Insert
```javascript
const items = [
{ name: 'Item 1', value: 100 },
{ name: 'Item 2', value: 200 },
{ name: 'Item 3', value: 300 },
];
const inserted = await insert({
table: 'table_name',
data: items, // Pass array for batch insert
returning: '*',
});
```
---
## Flow & Architecture
### How Query Helper Works
```
┌─────────────────────────────────────────────────────────────┐
│ 1. Route Handler (routes/userRoutes.js) │
│ - Receives HTTP request │
│ - Extracts parameters from req.body/req.params │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 2. Query Helper Call │
│ - Calls select/insert/update/deleteRecord/execute │
│ - Passes JSON options object │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 3. Query Helper (db/queryHelper/index.js) │
│ - Validates table name (whitelist check) │
│ - Validates options (required fields, types) │
│ - Applies WHERE conditions (JSON → Knex) │
│ - Builds query using Knex.js │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 4. Knex.js (db/queryHelper/knex.js) │
│ - Converts query builder to parameterized SQL │
│ - Executes query on PostgreSQL │
│ - Returns results │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 5. Response │
│ - Query Helper returns results │
│ - Route handler formats response │
│ - Sends JSON response to client │
└─────────────────────────────────────────────────────────────┘
```
### Data Flow Example: Creating a User
```
1. Client Request
POST /users
{ "name": "John", "phone_number": "+1234567890" }
2. Route Handler (userRoutes.js)
- Extracts: name, phone_number from req.body
- Builds: userData = { name, phone_number, ... }
3. Query Helper Call
insert({
table: 'users',
data: userData,
returning: '*'
})
4. Query Helper Processing
- Validates: table 'users' is in whitelist ✓
- Validates: data is object ✓
- Builds: Knex insert query
5. Knex.js Execution
- Generates: INSERT INTO users (name, phone_number, ...) VALUES ($1, $2, ...) RETURNING *
- Executes: Parameterized query on PostgreSQL
6. Database Response
- Returns: { id: 'uuid', name: 'John', phone_number: '+1234567890', ... }
7. Route Handler Response
- Returns: res.status(201).json(user)
8. Client Receives
201 Created
{ "id": "uuid", "name": "John", "phone_number": "+1234567890", ... }
```
### Transaction Flow
```
┌─────────────────────────────────────────────────────────────┐
│ execute({ type: 'transaction', handler: async (trx) => { }) │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ BEGIN TRANSACTION │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Execute handler function │
│ - All queries use 'trx' instead of 'db' │
│ - If any query fails, entire transaction rolls back │
└──────────────────────┬──────────────────────────────────────┘
┌─────────────┴─────────────┐
│ │
▼ ▼
┌─────────┐ ┌─────────┐
│ SUCCESS │ │ ERROR │
└────┬────┘ └────┬────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ COMMIT │ │ ROLLBACK │
│ Return result │ │ Throw error │
└─────────────────┘ └─────────────────┘
```
---
## Best Practices
### 1. Always Check for Deleted Records
```javascript
// ✅ Good
const users = await select({
table: 'users',
where: {
id: id,
deleted: false, // Always include this
},
});
// ❌ Bad
const users = await select({
table: 'users',
where: { id: id }, // Missing deleted check
});
```
### 2. Cap Limits to Prevent Resource Exhaustion
```javascript
// ✅ Good
const limit = Math.min(req.query.limit || 20, 100);
// ❌ Bad
const limit = req.query.limit || 20; // No cap
```
### 3. Use Transactions for Multi-Step Operations
```javascript
// ✅ Good - All or nothing
await execute({
type: 'transaction',
handler: async (trx) => {
await trx('table1').insert(data1);
await trx('table2').insert(data2);
},
});
// ❌ Bad - Partial failures possible
await insert({ table: 'table1', data: data1 });
await insert({ table: 'table2', data: data2 });
```
### 4. Handle Partial Updates Correctly
```javascript
// ✅ Good - Only update provided fields
const updateData = {};
if (req.body.name !== undefined) updateData.name = req.body.name;
if (req.body.email !== undefined) updateData.email = req.body.email;
// ❌ Bad - Sends undefined values
const updateData = {
name: req.body.name, // Could be undefined
email: req.body.email, // Could be undefined
};
```
### 5. Always Use WHERE Clause for UPDATE/DELETE
```javascript
// ✅ Good - WHERE clause required
await update({
table: 'users',
data: { name: 'John' },
where: { id: userId }, // Required
});
// ❌ Bad - Missing WHERE (will fail with error)
await update({
table: 'users',
data: { name: 'John' },
// Missing where clause
});
```
### 6. Use Returning to Get Updated/Inserted Data
```javascript
// ✅ Good - Get the inserted/updated record
const user = await insert({
table: 'users',
data: userData,
returning: '*', // Get all fields back
});
// ❌ Bad - Don't know what was inserted
await insert({
table: 'users',
data: userData,
// No returning - have to query again
});
```
### 7. Validate Input Before Querying
```javascript
// ✅ Good - Validate first
if (!name || !phone_number) {
return res.status(400).json({ error: "Name and phone are required" });
}
const user = await insert({
table: 'users',
data: { name, phone_number },
});
// ❌ Bad - Let database handle validation
const user = await insert({
table: 'users',
data: { name, phone_number }, // Could be null/undefined
});
```
### 8. Use Appropriate Error Handling
```javascript
// ✅ Good - Specific error handling
try {
const user = await insert({ table: 'users', data: userData });
res.status(201).json(user);
} catch (err) {
if (err.code === "23505") {
return res.status(400).json({ error: "Duplicate entry" });
}
console.error("Error:", err);
res.status(500).json({ error: "Internal server error" });
}
// ❌ Bad - Generic error handling
try {
const user = await insert({ table: 'users', data: userData });
res.json(user);
} catch (err) {
res.status(500).json({ error: "Error" }); // Not helpful
}
```
---
## Troubleshooting
### Error: "Table 'xxx' is not allowed"
**Problem:** Table name not in whitelist.
**Solution:** Add table to `ALLOWED_TABLES` in `db/queryHelper/index.js`
```javascript
const ALLOWED_TABLES = new Set([
'users',
'listings',
// ... add your table here
'your_table_name',
]);
```
### Error: "WHERE clause is required"
**Problem:** UPDATE or DELETE called without WHERE clause.
**Solution:** Always include WHERE clause:
```javascript
// ✅ Correct
await update({
table: 'users',
data: { name: 'John' },
where: { id: userId }, // Required
});
// ❌ Wrong
await update({
table: 'users',
data: { name: 'John' },
// Missing where
});
```
### Error: "Unsupported operator: xxx"
**Problem:** Using an operator that's not supported.
**Solution:** Check supported operators list. For complex queries, use `execute()`:
```javascript
// For complex queries, use execute()
const results = await execute({
type: 'raw-builder',
handler: async (knex) => {
return await knex('table')
.where('column', 'your-operator', value);
},
});
```
### Results Not Found
**Problem:** Query returns empty array but record exists.
**Solution:** Check WHERE conditions, especially `deleted` flag:
```javascript
// Make sure you're not filtering out the record
const users = await select({
table: 'users',
where: {
id: id,
deleted: false, // Make sure this matches your data
},
});
```
### Transaction Not Rolling Back
**Problem:** Error in transaction but changes persist.
**Solution:** Ensure all queries use `trx` parameter:
```javascript
// ✅ Correct - Uses trx
await execute({
type: 'transaction',
handler: async (trx) => {
await trx('table1').insert(data1);
await trx('table2').insert(data2);
},
});
// ❌ Wrong - Uses db directly (not in transaction)
await execute({
type: 'transaction',
handler: async (trx) => {
await db('table1').insert(data1); // Wrong! Use trx
},
});
```
---
## Quick Reference
### Import Statement
```javascript
import { select, insert, update, deleteRecord, execute } from '../db/queryHelper/index.js';
```
### Common Patterns
```javascript
// SELECT
const results = await select({
table: 'table_name',
where: { deleted: false },
orderBy: { column: 'created_at', direction: 'desc' },
limit: 20,
});
// INSERT
const record = await insert({
table: 'table_name',
data: { field1: 'value1', field2: 'value2' },
returning: '*',
});
// UPDATE
const updated = await update({
table: 'table_name',
data: { field1: 'new_value' },
where: { id: recordId },
returning: '*',
});
// SOFT DELETE
const deleted = await update({
table: 'table_name',
data: { deleted: true },
where: { id: recordId },
});
// TRANSACTION
const result = await execute({
type: 'transaction',
handler: async (trx) => {
// Use trx for all queries
return await trx('table').insert(data).returning('*');
},
});
```
---
## Additional Resources
- **API Reference**: See `db/queryHelper/API_REFERENCE.md` for complete method documentation
- **Examples**: See `routes/userRoutes.js` for real implementation examples
- **Migration Guide**: See `db/queryHelper/MIGRATION.md` for migrating from raw SQL
---
## Summary
The Query Helper provides a clean, safe, and maintainable way to interact with the database:
1. **Import** the functions you need
2. **Build** JSON options objects
3. **Call** the appropriate function (select/insert/update/deleteRecord/execute)
4. **Handle** errors appropriately
5. **Return** results to client
No raw SQL strings needed! 🎉