Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@velkymx/ts-orm",
"version": "1.4.0",
"version": "1.5.0",
"description": "A lightweight CRUD ORM built on MySQL2 using JSON-based structs",
"type": "module",
"repository": {
Expand Down
202 changes: 188 additions & 14 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,69 @@ A lightweight, JSON-struct-based ORM for Node.js using MySQL2. Define your schem

- ✅ Define data schemas using simple JSON structs
- ✅ Validates payloads before writing to DB
- ✅ **SQL injection prevention** with identifier validation & escaping
- ✅ **Enhanced input validation** (UUID, datetime, date, boolean formats)
- ✅ **Sanitized error messages** for security
- ✅ Supports full CRUD (Create, Read, Update, Delete)
- ✅ Consistent JSON responses
- ✅ Built on MySQL2 + dotenv
- ✅ ESM-ready (ES6+ syntax)

---

## 🔒 Security (v1.5.0+)

**ts-orm** now includes enterprise-grade security features:

### SQL Injection Prevention
All table names, column names, and ORDER BY clauses are validated and escaped to prevent SQL injection attacks.

### Input Validation
- **UUID**: Validates proper UUID v4 format
- **Datetime**: Validates `YYYY-MM-DD HH:MM:SS` format with range checks
- **Date**: Validates `YYYY-MM-DD` format with range checks
- **Boolean**: Type-checks boolean values (accepts `true`, `false`, `0`, `1`)
- **Enum**: Properly handles `null`/`undefined` for non-required fields

### Error Sanitization
Database errors are sanitized to prevent schema information leakage:
- Duplicate key errors → "Record already exists"
- Foreign key violations → Safe descriptive messages
- Full errors logged server-side for debugging
- Generic messages returned to clients

---

## ⚠️ Breaking Changes in v1.5.0

### Identifier Validation
Table and column names must now contain only alphanumeric characters and underscores (`[a-zA-Z0-9_]`). Special characters like spaces, quotes, or semicolons will be rejected.

**Before v1.5.0**: Any identifier accepted (security risk)
**After v1.5.0**: Only safe identifiers allowed

```javascript
// ✅ Valid identifiers
await create('users', struct, payload);
await create('user_profiles', struct, payload);
await create('users_2024', struct, payload);

// ❌ Invalid identifiers (will throw error)
await create('users; DROP TABLE', struct, payload); // SQL injection attempt
await create('user profiles', struct, payload); // Contains space
await create('users-table', struct, payload); // Contains hyphen
```

### Error Messages
Error messages are now sanitized and more generic to prevent information leakage.

**Before v1.5.0**: `"DB Error: Duplicate entry 'john@example.com' for key 'users.email'"`
**After v1.5.0**: `"Record already exists"`

Full error details are logged server-side with `console.error()` for debugging.

---

## Installation

```bash
Expand Down Expand Up @@ -96,13 +152,33 @@ async function tryCreateUser() {

tryCreateUser();
```
### Output on Fail
* Failed to create user:
* Message: Validation failed
* Details: [ 'email is required' ]

### Output on Validation Failure
```
Failed to create user:
Message: Validation failed
Details: [ 'email is required' ]
```

### Output on Invalid UUID (v1.5.0+)
```
Failed to create user:
Message: Validation failed
Details: [ 'id must be a valid UUID' ]
```

### Output on Duplicate Key (v1.5.0+)
```
Failed to create user:
Message: Database operation failed
Details: Record already exists
```
*Note: Full error details are logged server-side for debugging*

### Output on Success
```
User created successfully: { id: 1 }
```

## Using ts-orm in an ExpressJS Controller

Expand Down Expand Up @@ -152,16 +228,38 @@ export default router;

## 📘 Struct Field Types

| Type | Description | Example Value | Notes |
|--------------|--------------------------------------|----------------------------------------|----------------------------------------------------|
| `uuid` | Universally unique identifier | `"123e4567-e89b-12d3-a456-426614174000"` | Must match UUID v4 format |
| `string` | Text string | `"Alice"` | `length` defines max characters allowed |
| `number` | Integer or float | `42` | Used for numeric fields including IDs |
| `datetime` | Date and time | `"2025-04-07 10:00:00"` | Format: `YYYY-MM-DD HH:MM:SS` |
| `date` | Date only | `"2025-04-07"` | Format: `YYYY-MM-DD` |
| `boolean` | Boolean flag | `true` or `false` | Stored as TINYINT in MySQL |
| `enum` | Predefined set of valid strings | `"percent"` or `"flat"` | Must define `enum: [...]` in struct |
| `auto_increment` | Special case for `number` fields | Not passed on insert | Use `"default": "auto_increment"` in struct config |
| Type | Description | Example Value | Validation (v1.5.0+) | Notes |
|--------------|--------------------------------------|----------------------------------------|------------------------------------------------------------------|----------------------------------------------------|
| `uuid` | Universally unique identifier | `"123e4567-e89b-12d3-a456-426614174000"` | **Must match UUID v4 format** (validated) | Regex pattern validated |
| `string` | Text string | `"Alice"` | Length validation only | `length` defines max characters allowed |
| `number` | Integer or float | `42` | Type validation (isNaN check) | Used for numeric fields including IDs |
| `datetime` | Date and time | `"2025-04-07 10:00:00"` | **Must match `YYYY-MM-DD HH:MM:SS` format** with range checks | Format and validity validated |
| `date` | Date only | `"2025-04-07"` | **Must match `YYYY-MM-DD` format** with range checks | Format and validity validated |
| `boolean` | Boolean flag | `true`, `false`, `0`, `1` | **Type-checked** (boolean, 0, 1, '0', '1' allowed) | Stored as TINYINT in MySQL |
| `enum` | Predefined set of valid strings | `"percent"` or `"flat"` | Validates value in enum array; allows null for non-required | Must define `enum: [...]` in struct |
| `auto_increment` | Special case for `number` fields | Not passed on insert | Cannot be manually provided on insert | Use `"default": "auto_increment"` in struct config |

### Validation Examples

```javascript
// ✅ Valid values (will pass validation)
const validPayload = {
id: '123e4567-e89b-12d3-a456-426614174000', // Valid UUID v4
created_at: '2025-04-07 10:30:00', // Valid datetime
birth_date: '1990-05-15', // Valid date
is_active: true, // Valid boolean
commission_type: 'percent' // Valid enum value
};

// ❌ Invalid values (will fail validation)
const invalidPayload = {
id: 'not-a-valid-uuid', // Invalid UUID format
created_at: '2025/04/07 10:30:00', // Invalid datetime format (slashes)
birth_date: '04-07-2025', // Invalid date format
is_active: 'yes', // Invalid boolean
commission_type: 'invalid' // Not in enum array
};
```


## Using the CLI Tool to Generate Structs
Expand All @@ -182,6 +280,82 @@ users.json

You can then use this file as the struct definition for `ts-orm` CRUD operations.

## 🔧 Migration Guide: v1.4.0 → v1.5.0

### Step 1: Verify Identifier Names
Ensure all your table and column names use only alphanumeric characters and underscores:

```javascript
// Check your table names
const validTables = ['users', 'user_profiles', 'orders_2024']; // ✅ Valid
const invalidTables = ['user-profiles', 'users table', 'orders#']; // ❌ Invalid

// Check your column names in structs
const struct = [
{ name: "user_id", ... }, // ✅ Valid
{ name: "created_at", ... }, // ✅ Valid
{ name: "user-name", ... }, // ❌ Invalid - contains hyphen
];
```

### Step 2: Update Error Handling
Error messages are now more generic. If you're parsing error messages, update your code:

```javascript
// Before v1.5.0
if (result.data.includes('Duplicate entry')) { ... }

// After v1.5.0
if (result.data === 'Record already exists') { ... }
```

### Step 3: Review Validation Requirements
New validation is stricter. Ensure your payloads match the expected formats:

```javascript
// UUIDs must be valid v4 format
id: '123e4567-e89b-12d3-a456-426614174000' // ✅

// Datetimes must be YYYY-MM-DD HH:MM:SS
created_at: '2025-04-07 10:00:00' // ✅
created_at: '2025/04/07 10:00:00' // ❌

// Dates must be YYYY-MM-DD
birth_date: '1990-05-15' // ✅
birth_date: '05/15/1990' // ❌

// Booleans must be proper type
is_active: true // ✅
is_active: 1 // ✅
is_active: 'yes' // ❌
```

### Step 4: Test Your Application
Run your test suite to catch any validation errors:

```bash
npm test
```

## 📝 Changelog

### v1.5.0 (2025-12-11)
**Security Enhancements**
- ✅ Added SQL injection prevention with hybrid validation & escaping
- ✅ Enhanced input validation (UUID, datetime, date, boolean formats)
- ✅ Implemented error message sanitization
- ✅ Added comprehensive security test suite

**Breaking Changes**
- ⚠️ Table/column names now restricted to `[a-zA-Z0-9_]`
- ⚠️ Error messages are now sanitized (more generic)
- ⚠️ Stricter type validation for UUID, datetime, date, boolean fields

**New Features**
- Server-side error logging with `console.error()`
- Improved validation error messages
- Better handling of null/undefined in non-required enum fields

## 🌐 TechnoSorcery.com

Built with ✨ by [TechnoSorcery.com](https://technosorcery.com)
5 changes: 4 additions & 1 deletion src/introspect.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import mysql from 'mysql2/promise';
import dotenv from 'dotenv';
import { validateAndEscapeIdentifier } from './security.js';
dotenv.config();

const typeMap = {
Expand All @@ -24,7 +25,9 @@ export async function generateStructFromTable(table) {
database: process.env.DB_DATABASE
});

const [columns] = await pool.execute(`SHOW COLUMNS FROM \`${table}\``);
// Validate and escape table name
const safeTable = validateAndEscapeIdentifier(table, 'table name');
const [columns] = await pool.execute(`SHOW COLUMNS FROM ${safeTable}`);

const struct = columns.map(col => {
const rawType = col.Type.toLowerCase();
Expand Down
Loading
Loading