Add parameterized SQL queries with nginx variables
New Feature: sqlite_param Directive - Allows passing nginx variables as SQL prepared statement parameters - Supports query parameters ($arg_name), path captures ($1, $2), headers, etc. - Safe SQL injection protection via rusqlite prepared statements - Multiple parameters supported (bound in order to ? placeholders) Implementation: - New sqlite_param directive for adding query parameters - Variable resolution using ngx_http_get_variable() FFI - UTF-8 validation on all variable values - Updated execute_query() to accept parameter array - rusqlite::ToSql parameter binding Examples: - Book detail by ID: /book?id=1 - Genre filtering: /genre?genre=Programming - Year range search: /years?min=2015&max=2024 New Files: - conf/book_detail.conf: Parameter examples configuration - server_root/book/detail.hbs: Book detail page template - server_root/genre/genre.hbs: Genre filter page template - start_book_detail.sh: Quick start script for params example - README.md: Comprehensive project documentation - README_PARAMETERS.md: Parameters feature documentation Configuration: - MainConfig now supports global_templates_dir - ModuleConfig extended with query_params Vec - Handler resolves variables at request time - Template paths adjusted for exact location matches All examples tested and working with both static and parameterized queries.
This commit is contained in:
294
README.md
Normal file
294
README.md
Normal file
@ -0,0 +1,294 @@
|
||||
# nginx-test - SQLite Module for NGINX
|
||||
|
||||
A dynamic NGINX module written in Rust that integrates SQLite databases with Handlebars templating, enabling data-driven web applications directly from NGINX configuration.
|
||||
|
||||
## Features
|
||||
|
||||
✅ **SQLite Integration** - Query SQLite databases from NGINX
|
||||
✅ **Handlebars Templates** - Render dynamic HTML with template inheritance
|
||||
✅ **Parameterized Queries** - Safe SQL parameters from nginx variables
|
||||
✅ **Global & Local Templates** - Template reuse with override support
|
||||
✅ **Zero Application Server** - Serve data-driven pages directly from NGINX
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Build the Module
|
||||
|
||||
```bash
|
||||
direnv exec "$PWD" cargo build
|
||||
```
|
||||
|
||||
### 2. Run the Book Catalog Example
|
||||
|
||||
```bash
|
||||
./start_book_catalog.sh
|
||||
```
|
||||
|
||||
Visit http://localhost:8080/books/all
|
||||
|
||||
### 3. Run the Parameters Example
|
||||
|
||||
```bash
|
||||
./start_book_detail.sh
|
||||
```
|
||||
|
||||
Visit http://localhost:8081/book?id=1
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Book Catalog (Port 8080)
|
||||
|
||||
A full-featured catalog with category browsing, global templates, and responsive UI.
|
||||
|
||||
**Features:**
|
||||
- Browse all books or filter by category
|
||||
- Shared header/footer/card templates
|
||||
- Modern gradient UI design
|
||||
- Multiple category pages
|
||||
|
||||
**See:** `conf/book_catalog.conf` and `README_BOOK_CATALOG.md`
|
||||
|
||||
### Example 2: Parameterized Queries (Port 8081)
|
||||
|
||||
Demonstrates dynamic SQL queries with nginx variables.
|
||||
|
||||
**Features:**
|
||||
- Book detail pages by ID
|
||||
- Genre filtering with query parameters
|
||||
- Year range searches with multiple parameters
|
||||
- Safe prepared statement parameter binding
|
||||
|
||||
**See:** `conf/book_detail.conf` and `README_PARAMETERS.md`
|
||||
|
||||
## Configuration Directives
|
||||
|
||||
### `sqlite_db`
|
||||
Set the SQLite database file path.
|
||||
|
||||
**Syntax:** `sqlite_db path;`
|
||||
**Context:** `location`
|
||||
|
||||
### `sqlite_query`
|
||||
Define the SQL SELECT query to execute.
|
||||
|
||||
**Syntax:** `sqlite_query "SELECT ...";`
|
||||
**Context:** `location`
|
||||
**Notes:** Use `?` placeholders for parameters
|
||||
|
||||
### `sqlite_template`
|
||||
Specify the Handlebars template file (relative to location path).
|
||||
|
||||
**Syntax:** `sqlite_template filename.hbs;`
|
||||
**Context:** `location`
|
||||
**Notes:** Sets the content handler for the location
|
||||
|
||||
### `sqlite_param`
|
||||
Add a parameter to the SQL query (can be used multiple times).
|
||||
|
||||
**Syntax:** `sqlite_param $variable_or_value;`
|
||||
**Context:** `location`
|
||||
**Notes:** Order matches `?` placeholders in query
|
||||
|
||||
### `sqlite_global_templates`
|
||||
Set a directory for global template files (partials, layouts).
|
||||
|
||||
**Syntax:** `sqlite_global_templates directory;`
|
||||
**Context:** `http`
|
||||
|
||||
## Basic Example
|
||||
|
||||
```nginx
|
||||
http {
|
||||
sqlite_global_templates "templates/global";
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
root "public";
|
||||
|
||||
# Simple query without parameters
|
||||
location = /books {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "SELECT * FROM books ORDER BY title";
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
|
||||
# Parameterized query
|
||||
location = /book {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id;
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Template System
|
||||
|
||||
### Template Resolution
|
||||
|
||||
Templates are resolved as: `{document_root}{uri}/{template_name}`
|
||||
|
||||
Example:
|
||||
- `root "public"`
|
||||
- `location /books`
|
||||
- `sqlite_template "list.hbs"`
|
||||
- Resolved to: `public/books/list.hbs`
|
||||
|
||||
### Global Templates
|
||||
|
||||
Place shared templates (headers, footers, partials) in a global directory:
|
||||
|
||||
```nginx
|
||||
http {
|
||||
sqlite_global_templates "templates/shared";
|
||||
}
|
||||
```
|
||||
|
||||
All `.hbs` files in this directory are automatically loaded as partials (referenced without `.hbs` extension):
|
||||
|
||||
```handlebars
|
||||
{{> header}}
|
||||
<div class="content">
|
||||
{{#each results}}
|
||||
{{> card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{> footer}}
|
||||
```
|
||||
|
||||
### Local Templates
|
||||
|
||||
Each location can have its own template directory. Local templates override global ones with the same name.
|
||||
|
||||
**Directory structure:**
|
||||
```
|
||||
public/
|
||||
├── global/ # Global templates
|
||||
│ ├── header.hbs
|
||||
│ └── footer.hbs
|
||||
└── books/
|
||||
├── list.hbs # Main template
|
||||
└── card.hbs # Local partial (overrides global if exists)
|
||||
```
|
||||
|
||||
### Template Data
|
||||
|
||||
Query results are passed to templates as a `results` array:
|
||||
|
||||
```handlebars
|
||||
<h1>Books ({{results.length}} total)</h1>
|
||||
<ul>
|
||||
{{#each results}}
|
||||
<li>{{title}} by {{author}} ({{year}})</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
```
|
||||
|
||||
## SQL Query Results
|
||||
|
||||
Results are converted to JSON format:
|
||||
|
||||
| SQLite Type | JSON Type |
|
||||
|-------------|-----------|
|
||||
| NULL | `null` |
|
||||
| INTEGER | Number |
|
||||
| REAL | Number |
|
||||
| TEXT | String |
|
||||
| BLOB | String (hex-encoded) |
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
direnv exec "$PWD" cargo build
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
# Run nginx with configuration
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_catalog.conf -p .
|
||||
|
||||
# Test endpoint
|
||||
curl http://localhost:8080/books/all
|
||||
|
||||
# Stop nginx
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_catalog.conf -p .
|
||||
```
|
||||
|
||||
### Debug
|
||||
|
||||
Enable debug logging in nginx configuration:
|
||||
|
||||
```nginx
|
||||
error_log logs/error.log debug;
|
||||
```
|
||||
|
||||
Then check the logs:
|
||||
|
||||
```bash
|
||||
tail -f logs/error.log | grep sqlite
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Request → NGINX → Module Handler → SQLite Query
|
||||
↓
|
||||
Resolve Variables
|
||||
↓
|
||||
Execute Prepared Statement
|
||||
↓
|
||||
Load Templates (Global + Local)
|
||||
↓
|
||||
Render with Handlebars
|
||||
↓
|
||||
Return HTML Response
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
nginx-test/
|
||||
├── src/
|
||||
│ └── lib.rs # Module implementation
|
||||
├── conf/
|
||||
│ ├── book_catalog.conf # Static catalog example
|
||||
│ └── book_detail.conf # Parameterized queries example
|
||||
├── server_root/
|
||||
│ ├── global_templates/ # Shared templates
|
||||
│ │ ├── header.hbs
|
||||
│ │ ├── footer.hbs
|
||||
│ │ └── book_card.hbs
|
||||
│ ├── books/ # Category pages
|
||||
│ ├── book/ # Detail pages
|
||||
│ └── genre/ # Genre pages
|
||||
├── book_catalog.db # Sample database
|
||||
├── setup_book_catalog.sh # Database setup script
|
||||
├── start_book_catalog.sh # Start catalog server
|
||||
├── start_book_detail.sh # Start parameters example
|
||||
├── README_BOOK_CATALOG.md # Catalog example docs
|
||||
└── README_PARAMETERS.md # Parameters feature docs
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Rust** - 2024 edition
|
||||
- **ngx** (0.5.0) - Rust bindings for NGINX
|
||||
- **rusqlite** (0.37.0) - SQLite integration
|
||||
- **handlebars** (6.3.2) - Template engine
|
||||
- **serde** & **serde_json** - JSON serialization
|
||||
|
||||
## License
|
||||
|
||||
See LICENSE file for details.
|
||||
|
||||
## Resources
|
||||
|
||||
- [NGINX Module Development Guide](https://nginx.org/en/docs/dev/development_guide.html)
|
||||
- [ngx Rust Crate](https://crates.io/crates/ngx)
|
||||
- [Handlebars Rust](https://crates.io/crates/handlebars)
|
||||
- [Rusqlite](https://crates.io/crates/rusqlite)
|
||||
|
||||
276
README_PARAMETERS.md
Normal file
276
README_PARAMETERS.md
Normal file
@ -0,0 +1,276 @@
|
||||
# Path Parameters Feature
|
||||
|
||||
The nginx-test SQLite module supports parameterized SQL queries using nginx variables. This allows you to pass dynamic values from the request (query parameters, path captures, headers, etc.) as safe SQL prepared statement parameters.
|
||||
|
||||
## New Directive
|
||||
|
||||
### `sqlite_param`
|
||||
|
||||
Add parameters to SQL queries. Can be used multiple times to add multiple parameters.
|
||||
|
||||
**Syntax:** `sqlite_param variable_or_value;`
|
||||
**Context:** `location`
|
||||
**Multiple:** Yes (order matches `?` placeholders in query)
|
||||
|
||||
## Usage
|
||||
|
||||
### Query Parameters (Most Common)
|
||||
|
||||
Use nginx's built-in `$arg_*` variables to access query parameters:
|
||||
|
||||
```nginx
|
||||
location = /book {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id; # Gets ?id=123 from URL
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/book?id=5`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE id = '5'`
|
||||
|
||||
### Multiple Parameters
|
||||
|
||||
Parameters are bound to `?` placeholders in order:
|
||||
|
||||
```nginx
|
||||
location = /years {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE year >= ? AND year <= ?";
|
||||
sqlite_param $arg_min; # First ? placeholder
|
||||
sqlite_param $arg_max; # Second ? placeholder
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/years?min=2015&max=2024`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE year >= '2015' AND year <= '2024'`
|
||||
|
||||
### Regex Path Captures
|
||||
|
||||
Use numbered captures (`$1`, `$2`, etc.) from regex locations:
|
||||
|
||||
```nginx
|
||||
location ~ ^/book/([0-9]+)$ {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $1; # First capture group
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/book/5`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE id = '5'`
|
||||
|
||||
### Named Captures
|
||||
|
||||
Use named captures from regex locations:
|
||||
|
||||
```nginx
|
||||
location ~ ^/author/(?<author_name>[^/]+)/books$ {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE author LIKE ?";
|
||||
sqlite_param $author_name;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
**Request:** `http://localhost/author/Martin/books`
|
||||
**SQL Executed:** `SELECT * FROM books WHERE author LIKE 'Martin'`
|
||||
|
||||
### Other Nginx Variables
|
||||
|
||||
Any nginx variable can be used as a parameter:
|
||||
|
||||
```nginx
|
||||
location = /search {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE title LIKE '%' || ? || '%'";
|
||||
sqlite_param $arg_q; # Query string parameter
|
||||
sqlite_template "search.hbs";
|
||||
}
|
||||
|
||||
location = /client-info {
|
||||
sqlite_db "access_log.db";
|
||||
sqlite_query "INSERT INTO visits (ip, user_agent) VALUES (?, ?)";
|
||||
sqlite_param $remote_addr; # Client IP
|
||||
sqlite_param $http_user_agent; # User agent header
|
||||
sqlite_template "logged.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
### Literal Values
|
||||
|
||||
You can also use literal values (though less common):
|
||||
|
||||
```nginx
|
||||
location = /featured {
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE rating >= ? ORDER BY rating DESC";
|
||||
sqlite_param "4.5"; # Literal value
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
## Available Nginx Variables
|
||||
|
||||
Common nginx variables you can use as parameters:
|
||||
|
||||
### Query String
|
||||
- `$arg_name` - Query parameter (e.g., `?name=value`)
|
||||
- `$args` - Full query string
|
||||
- `$query_string` - Same as `$args`
|
||||
|
||||
### Request Info
|
||||
- `$request_method` - GET, POST, etc.
|
||||
- `$request_uri` - Full request URI with query string
|
||||
- `$uri` - Request URI without query string
|
||||
- `$document_uri` - Same as `$uri`
|
||||
|
||||
### Client Info
|
||||
- `$remote_addr` - Client IP address
|
||||
- `$remote_port` - Client port
|
||||
- `$remote_user` - HTTP basic auth username
|
||||
|
||||
### Headers
|
||||
- `$http_name` - Any HTTP header (e.g., `$http_user_agent`, `$http_referer`)
|
||||
- `$content_type` - Content-Type header
|
||||
- `$content_length` - Content-Length header
|
||||
|
||||
### Path Captures
|
||||
- `$1`, `$2`, ..., `$9` - Numbered regex captures
|
||||
- `$name` - Named regex captures (`(?<name>...)`)
|
||||
|
||||
### Server Info
|
||||
- `$server_name` - Server name
|
||||
- `$server_port` - Server port
|
||||
- `$scheme` - http or https
|
||||
- `$hostname` - Hostname
|
||||
|
||||
See [nginx variables documentation](http://nginx.org/en/docs/http/ngx_http_core_module.html#variables) for complete list.
|
||||
|
||||
## Security
|
||||
|
||||
**SQL Injection Protection:**
|
||||
- All parameters are passed through SQLite's prepared statement mechanism
|
||||
- Values are properly escaped and quoted by SQLite
|
||||
- **SAFE:** `sqlite_param $arg_id` with query `SELECT * FROM books WHERE id = ?`
|
||||
- **SAFE:** Multiple parameters are bound separately to each `?`
|
||||
|
||||
**Never concatenate variables into the query string:**
|
||||
- **UNSAFE:** `sqlite_query "SELECT * FROM books WHERE id = $arg_id"` ❌
|
||||
- **SAFE:** Use `sqlite_param` instead ✓
|
||||
|
||||
## Examples
|
||||
|
||||
### Book Detail Page
|
||||
|
||||
```nginx
|
||||
location = /book {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id;
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
Visit: `http://localhost/book?id=42`
|
||||
|
||||
### Search by Multiple Criteria
|
||||
|
||||
```nginx
|
||||
location = /search {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "
|
||||
SELECT * FROM books
|
||||
WHERE title LIKE '%' || ? || '%'
|
||||
AND year >= ?
|
||||
AND rating >= ?
|
||||
ORDER BY rating DESC
|
||||
";
|
||||
sqlite_param $arg_title;
|
||||
sqlite_param $arg_year;
|
||||
sqlite_param $arg_rating;
|
||||
sqlite_template "results.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
Visit: `http://localhost/search?title=rust&year=2020&rating=4.5`
|
||||
|
||||
### Category with Pagination
|
||||
|
||||
```nginx
|
||||
location = /category {
|
||||
sqlite_db "catalog.db";
|
||||
sqlite_query "
|
||||
SELECT * FROM books
|
||||
WHERE genre = ?
|
||||
ORDER BY title
|
||||
LIMIT ? OFFSET ?
|
||||
";
|
||||
sqlite_param $arg_genre;
|
||||
sqlite_param $arg_limit;
|
||||
sqlite_param $arg_offset;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
```
|
||||
|
||||
Visit: `http://localhost/category?genre=Programming&limit=10&offset=0`
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Missing Parameters
|
||||
|
||||
If a required nginx variable is not set, the module returns `400 Bad Request`:
|
||||
|
||||
```nginx
|
||||
location = /book {
|
||||
sqlite_param $arg_id; # If ?id= is not provided
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** 400 Bad Request
|
||||
|
||||
### Invalid SQL
|
||||
|
||||
If parameter values cause SQL errors (e.g., type mismatch), returns `500 Internal Server Error`:
|
||||
|
||||
```nginx
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id; # If ?id=abc (not a number)
|
||||
```
|
||||
|
||||
**Response:** 500 Internal Server Error (check nginx error log)
|
||||
|
||||
### Variable Not Found
|
||||
|
||||
If a variable name doesn't exist in nginx, returns `400 Bad Request` with log message.
|
||||
|
||||
## Complete Example
|
||||
|
||||
See `conf/book_detail.conf` for a working example with:
|
||||
- Single parameter (book by ID)
|
||||
- String parameter (genre filtering)
|
||||
- Multiple parameters (year range search)
|
||||
|
||||
Run it with:
|
||||
```bash
|
||||
./start_book_detail.sh
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Parameters are resolved at request time using `ngx_http_get_variable()`
|
||||
- UTF-8 validation is performed on all variable values
|
||||
- Parameters are bound using rusqlite's prepared statement API
|
||||
- All SQL placeholders must be `?` (positional parameters)
|
||||
- Parameters match placeholders in order of `sqlite_param` directives
|
||||
|
||||
## Limitations
|
||||
|
||||
- Only supports `?` positional parameters (not named parameters like `:name`)
|
||||
- Parameters must be provided in the exact order they appear in the query
|
||||
- All parameter values are treated as strings (SQLite performs type coercion)
|
||||
- Complex SQL values (arrays, JSON) should be constructed in the query itself
|
||||
|
||||
58
conf/book_detail.conf
Normal file
58
conf/book_detail.conf
Normal file
@ -0,0 +1,58 @@
|
||||
# Book Detail Configuration
|
||||
# Demonstrates using path parameters with the SQLite module
|
||||
|
||||
load_module target/debug/libnginx_test.dylib;
|
||||
|
||||
worker_processes 1;
|
||||
events {}
|
||||
|
||||
error_log logs/error.log debug;
|
||||
|
||||
http {
|
||||
# Global templates for shared components
|
||||
sqlite_global_templates "server_root/global_templates";
|
||||
|
||||
server {
|
||||
listen 8081;
|
||||
|
||||
root "server_root";
|
||||
|
||||
# Book detail page using a path parameter (numbered capture)
|
||||
# Captures the book ID from the URL and passes it to the SQL query
|
||||
location = /book {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = ?";
|
||||
sqlite_param $arg_id;
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
|
||||
# Books by genre using query parameter
|
||||
location = /genre {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE genre = ? ORDER BY rating DESC";
|
||||
sqlite_param $arg_genre;
|
||||
sqlite_template "genre.hbs";
|
||||
}
|
||||
|
||||
# Search by year range using query parameters
|
||||
location = /years {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE year >= ? AND year <= ? ORDER BY year DESC, title";
|
||||
sqlite_param $arg_min;
|
||||
sqlite_param $arg_max;
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
|
||||
# Fallback to all books
|
||||
location / {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books ORDER BY rating DESC, title";
|
||||
sqlite_template "list.hbs";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
140
server_root/book/detail.hbs
Normal file
140
server_root/book/detail.hbs
Normal file
@ -0,0 +1,140 @@
|
||||
{{> header}}
|
||||
|
||||
<style>
|
||||
.book-detail {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
.book-cover {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
height: 300px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.book-title {
|
||||
font-size: 2.5rem;
|
||||
color: #2d3748;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.book-author {
|
||||
font-size: 1.5rem;
|
||||
color: #667eea;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.book-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.meta-item {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.meta-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.meta-value {
|
||||
font-size: 1.25rem;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
}
|
||||
.book-description {
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
line-height: 1.8;
|
||||
font-size: 1.125rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
.rating {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.stars {
|
||||
color: #fbbf24;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.back-link:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
|
||||
{{#if results.[0]}}
|
||||
{{#with results.[0]}}
|
||||
<div class="book-detail">
|
||||
<div class="book-cover">
|
||||
📖
|
||||
</div>
|
||||
|
||||
<h1 class="book-title">{{title}}</h1>
|
||||
<p class="book-author">by {{author}}</p>
|
||||
|
||||
<div class="book-meta">
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Rating</div>
|
||||
<div class="meta-value rating">
|
||||
<span class="stars">⭐</span>
|
||||
{{rating}} / 5.0
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Published</div>
|
||||
<div class="meta-value">{{year}}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Genre</div>
|
||||
<div class="meta-value">{{genre}}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">ISBN</div>
|
||||
<div class="meta-value" style="font-family: monospace; font-size: 1rem;">{{isbn}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-description">
|
||||
<strong>About this book:</strong><br><br>
|
||||
{{description}}
|
||||
</div>
|
||||
|
||||
<a href="/books/all" class="back-link">← Back to Catalog</a>
|
||||
</div>
|
||||
{{/with}}
|
||||
{{else}}
|
||||
<div class="not-found">
|
||||
<h2>Book Not Found</h2>
|
||||
<p>The requested book could not be found in our catalog.</p>
|
||||
<a href="/books/all" class="back-link">← Back to Catalog</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{> footer}}
|
||||
|
||||
111
server_root/genre/genre.hbs
Normal file
111
server_root/genre/genre.hbs
Normal file
@ -0,0 +1,111 @@
|
||||
{{> header}}
|
||||
|
||||
<style>
|
||||
.book-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.book-card {
|
||||
background: #fff;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.book-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
|
||||
border-color: #667eea;
|
||||
}
|
||||
.book-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.book-title {
|
||||
color: #2d3748;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
.book-rating {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.book-author {
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.book-description {
|
||||
color: #4a5568;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.book-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.book-genre {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.book-year {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.book-isbn {
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.genre-header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.genre-header h2 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="genre-header">
|
||||
<h2>{{#if results.[0]}}{{results.[0].genre}}{{else}}Genre{{/if}} Books</h2>
|
||||
<p>Browse books in this category</p>
|
||||
</div>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
<a href="/book/{{id}}" style="text-decoration: none; color: inherit;">
|
||||
{{> book_card}}
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{#unless results}}
|
||||
<p style="text-align: center; color: #6c757d; padding: 2rem;">No books found in this genre.</p>
|
||||
{{/unless}}
|
||||
|
||||
{{> footer}}
|
||||
|
||||
122
server_root/list.hbs
Normal file
122
server_root/list.hbs
Normal file
@ -0,0 +1,122 @@
|
||||
{{> header}}
|
||||
|
||||
<style>
|
||||
.book-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.book-card {
|
||||
background: #fff;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.book-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
|
||||
border-color: #667eea;
|
||||
}
|
||||
.book-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.book-title {
|
||||
color: #2d3748;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
.book-rating {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.book-author {
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.book-description {
|
||||
color: #4a5568;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.book-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.book-genre {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.book-year {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.book-isbn {
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.stats {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-item h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.stat-item p {
|
||||
opacity: 0.9;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<h2>📚</h2>
|
||||
<p>Book Collection</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h2>⭐</h2>
|
||||
<p>Top Rated</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h2>🔍</h2>
|
||||
<p>Browse All</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="color: #2d3748; margin-bottom: 1rem;">All Books in Collection</h2>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
{{> book_card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{> footer}}
|
||||
|
||||
122
server_root/years/list.hbs
Normal file
122
server_root/years/list.hbs
Normal file
@ -0,0 +1,122 @@
|
||||
{{> header}}
|
||||
|
||||
<style>
|
||||
.book-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.book-card {
|
||||
background: #fff;
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.book-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
|
||||
border-color: #667eea;
|
||||
}
|
||||
.book-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.book-title {
|
||||
color: #2d3748;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
.book-rating {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.book-author {
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.book-description {
|
||||
color: #4a5568;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.book-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
.book-genre {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.book-year {
|
||||
background: #e9ecef;
|
||||
color: #495057;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.book-isbn {
|
||||
color: #6c757d;
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.stats {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
text-align: center;
|
||||
}
|
||||
.stat-item h2 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
.stat-item p {
|
||||
opacity: 0.9;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="stats">
|
||||
<div class="stat-item">
|
||||
<h2>📚</h2>
|
||||
<p>Book Collection</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h2>⭐</h2>
|
||||
<p>Top Rated</p>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<h2>🔍</h2>
|
||||
<p>Browse All</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="color: #2d3748; margin-bottom: 1rem;">All Books in Collection</h2>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
{{> book_card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
{{> footer}}
|
||||
|
||||
111
src/lib.rs
111
src/lib.rs
@ -1,10 +1,10 @@
|
||||
use handlebars::Handlebars;
|
||||
use ngx::core::Buffer;
|
||||
use ngx::ffi::{
|
||||
NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF, NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE,
|
||||
NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE, nginx_version, ngx_chain_t,
|
||||
ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t, ngx_module_t, ngx_str_t,
|
||||
ngx_uint_t,
|
||||
ngx_hash_key, ngx_http_get_variable, NGX_CONF_TAKE1, NGX_HTTP_LOC_CONF,
|
||||
NGX_HTTP_MAIN_CONF, NGX_HTTP_MODULE, NGX_HTTP_LOC_CONF_OFFSET, NGX_RS_MODULE_SIGNATURE,
|
||||
nginx_version, ngx_chain_t, ngx_command_t, ngx_conf_t, ngx_http_module_t, ngx_int_t,
|
||||
ngx_module_t, ngx_str_t, ngx_uint_t,
|
||||
};
|
||||
use ngx::http::{
|
||||
HttpModule, HttpModuleLocationConf, HttpModuleMainConf, MergeConfigError, NgxHttpCoreModule,
|
||||
@ -46,6 +46,7 @@ struct ModuleConfig {
|
||||
db_path: String,
|
||||
query: String,
|
||||
template_path: String,
|
||||
query_params: Vec<String>, // Variable names to use as query parameters
|
||||
}
|
||||
|
||||
// Global configuration for shared templates
|
||||
@ -78,6 +79,10 @@ impl http::Merge for ModuleConfig {
|
||||
self.template_path = prev.template_path.clone();
|
||||
}
|
||||
|
||||
if self.query_params.is_empty() {
|
||||
self.query_params = prev.query_params.clone();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -139,7 +144,7 @@ pub static mut ngx_http_howto_module: ngx_module_t = ngx_module_t {
|
||||
// sure to terminate the array with an empty command.
|
||||
#[unsafe(no_mangle)]
|
||||
#[allow(non_upper_case_globals)]
|
||||
static mut ngx_http_howto_commands: [ngx_command_t; 5] = [
|
||||
static mut ngx_http_howto_commands: [ngx_command_t; 6] = [
|
||||
ngx_command_t {
|
||||
name: ngx_string!("sqlite_global_templates"),
|
||||
type_: (NGX_HTTP_MAIN_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
|
||||
@ -172,6 +177,14 @@ static mut ngx_http_howto_commands: [ngx_command_t; 5] = [
|
||||
offset: 0,
|
||||
post: std::ptr::null_mut(),
|
||||
},
|
||||
ngx_command_t {
|
||||
name: ngx_string!("sqlite_param"),
|
||||
type_: (NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1) as ngx_uint_t,
|
||||
set: Some(ngx_http_howto_commands_add_param),
|
||||
conf: NGX_HTTP_LOC_CONF_OFFSET,
|
||||
offset: 0,
|
||||
post: std::ptr::null_mut(),
|
||||
},
|
||||
ngx_command_t {
|
||||
name: ngx_str_t {
|
||||
len: 0,
|
||||
@ -250,6 +263,22 @@ extern "C" fn ngx_http_howto_commands_set_template_path(
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
extern "C" fn ngx_http_howto_commands_add_param(
|
||||
cf: *mut ngx_conf_t,
|
||||
_cmd: *mut ngx_command_t,
|
||||
conf: *mut c_void,
|
||||
) -> *mut c_char {
|
||||
unsafe {
|
||||
let conf = &mut *(conf as *mut ModuleConfig);
|
||||
let args = (*(*cf).args).elts as *mut ngx_str_t;
|
||||
let param = (*args.add(1)).to_string();
|
||||
conf.query_params.push(param);
|
||||
};
|
||||
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
|
||||
// Load all .hbs templates from a directory into the Handlebars registry
|
||||
fn load_templates_from_dir(reg: &mut Handlebars, dir_path: &str) -> std::io::Result<usize> {
|
||||
use std::fs;
|
||||
@ -285,8 +314,12 @@ fn load_templates_from_dir(reg: &mut Handlebars, dir_path: &str) -> std::io::Res
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// Execute a generic SQL query and return results as JSON-compatible data
|
||||
fn execute_query(db_path: &str, query: &str) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
|
||||
// Execute a generic SQL query with parameters and return results as JSON-compatible data
|
||||
fn execute_query(
|
||||
db_path: &str,
|
||||
query: &str,
|
||||
params: &[&str],
|
||||
) -> Result<Vec<std::collections::HashMap<String, serde_json::Value>>> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
let mut stmt = conn.prepare(query)?;
|
||||
|
||||
@ -295,7 +328,13 @@ fn execute_query(db_path: &str, query: &str) -> Result<Vec<std::collections::Has
|
||||
.map(|i| stmt.column_name(i).unwrap_or("").to_string())
|
||||
.collect();
|
||||
|
||||
let rows = stmt.query_map([], |row| {
|
||||
// Convert params to rusqlite parameters
|
||||
let rusqlite_params: Vec<&dyn rusqlite::ToSql> = params
|
||||
.iter()
|
||||
.map(|p| p as &dyn rusqlite::ToSql)
|
||||
.collect();
|
||||
|
||||
let rows = stmt.query_map(rusqlite_params.as_slice(), |row| {
|
||||
let mut map = std::collections::HashMap::new();
|
||||
for (i, col_name) in column_names.iter().enumerate() {
|
||||
let value: serde_json::Value = match row.get_ref(i)? {
|
||||
@ -367,8 +406,60 @@ http_request_handler!(howto_access_handler, |request: &mut http::Request| {
|
||||
.and_then(|p| p.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Execute the configured SQL query
|
||||
let results = match execute_query(&co.db_path, &co.query) {
|
||||
// Resolve query parameters from nginx variables
|
||||
let _ = std::fs::write("/tmp/nginx_debug.txt", format!("Query: {}\nParams: {:?}\n", co.query, co.query_params));
|
||||
|
||||
let mut param_values: Vec<String> = Vec::new();
|
||||
for var_name in &co.query_params {
|
||||
let value = if var_name.starts_with('$') {
|
||||
// It's a variable reference, resolve it from nginx
|
||||
let var_name_str = &var_name[1..]; // Remove the '$' prefix
|
||||
let var_name_bytes = var_name_str.as_bytes();
|
||||
|
||||
let mut name = ngx_str_t {
|
||||
len: var_name_bytes.len(),
|
||||
data: var_name_bytes.as_ptr() as *mut u8,
|
||||
};
|
||||
|
||||
let key = unsafe { ngx_hash_key(name.data, name.len) };
|
||||
let r: *mut ngx::ffi::ngx_http_request_t = request.into();
|
||||
let var_value = unsafe { ngx_http_get_variable(r, &mut name, key) };
|
||||
|
||||
if var_value.is_null() {
|
||||
ngx_log_debug_http!(request, "variable not found: {}", var_name);
|
||||
return http::HTTPStatus::BAD_REQUEST.into();
|
||||
}
|
||||
|
||||
let var_ref = unsafe { &*var_value };
|
||||
if var_ref.valid() == 0 {
|
||||
ngx_log_debug_http!(request, "variable value not valid: {}", var_name);
|
||||
return http::HTTPStatus::BAD_REQUEST.into();
|
||||
}
|
||||
|
||||
match std::str::from_utf8(var_ref.as_bytes()) {
|
||||
Ok(s) => s.to_string(),
|
||||
Err(_) => {
|
||||
ngx_log_debug_http!(request, "failed to decode variable as UTF-8: {}", var_name);
|
||||
return http::HTTPStatus::INTERNAL_SERVER_ERROR.into();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a literal value
|
||||
var_name.clone()
|
||||
};
|
||||
param_values.push(value);
|
||||
}
|
||||
|
||||
ngx_log_debug_http!(
|
||||
request,
|
||||
"executing query with {} parameters: {:?}",
|
||||
param_values.len(),
|
||||
param_values
|
||||
);
|
||||
|
||||
// Execute the configured SQL query with parameters
|
||||
let param_refs: Vec<&str> = param_values.iter().map(|s| s.as_str()).collect();
|
||||
let results = match execute_query(&co.db_path, &co.query, ¶m_refs) {
|
||||
Ok(results) => results,
|
||||
Err(e) => {
|
||||
ngx_log_debug_http!(request, "failed to execute query: {}", e);
|
||||
|
||||
39
start_book_detail.sh
Executable file
39
start_book_detail.sh
Executable file
@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start script for the book detail example with path parameters
|
||||
|
||||
set -e
|
||||
|
||||
echo "📚 Starting Book Detail Example with Path Parameters..."
|
||||
echo ""
|
||||
|
||||
# Check if database exists
|
||||
if [ ! -f "book_catalog.db" ]; then
|
||||
echo "Database not found. Running setup..."
|
||||
./setup_book_catalog.sh
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if module is built
|
||||
if [ ! -f "target/debug/libnginx_test.dylib" ]; then
|
||||
echo "Module not built. Building..."
|
||||
direnv exec "$PWD" cargo build
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Start nginx
|
||||
echo "Starting nginx on http://localhost:8081"
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_detail.conf -p .
|
||||
|
||||
echo ""
|
||||
echo "✅ Book Detail Example is running!"
|
||||
echo ""
|
||||
echo "Try these URLs:"
|
||||
echo " • http://localhost:8081/book?id=1 - View book #1"
|
||||
echo " • http://localhost:8081/book?id=5 - View book #5"
|
||||
echo " • http://localhost:8081/genre?genre=Programming - Programming books"
|
||||
echo " • http://localhost:8081/genre?genre=Databases - Database books"
|
||||
echo " • http://localhost:8081/years?min=2000&max=2010 - Books from 2000-2010"
|
||||
echo " • http://localhost:8081/years?min=2015&max=2024 - Books from 2015-2024"
|
||||
echo ""
|
||||
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_detail.conf -p ."
|
||||
|
||||
Reference in New Issue
Block a user