From db1ed9ff578c669bdc8bd0b0236561a9d87dcb55 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:14:39 +0000 Subject: [PATCH 1/8] Initial plan From 66e337171bccccccacf2f8596e13b51ef0e5a8f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:21:22 +0000 Subject: [PATCH 2/8] Add comprehensive API documentation Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- README.md | 27 + docs/api/README.md | 1218 ++++++++++++++++++++++++++++++++++++ docs/api/authentication.md | 624 ++++++++++++++++++ docs/guide/index.md | 15 +- 4 files changed, 1883 insertions(+), 1 deletion(-) create mode 100644 docs/api/README.md create mode 100644 docs/api/authentication.md diff --git a/README.md b/README.md index ed85a541..9f66027b 100644 --- a/README.md +++ b/README.md @@ -211,6 +211,33 @@ aggregations: See [Visual Reporting Guide](./docs/guide/visual-reporting.md) for complete documentation. +## 📡 API Reference + +ObjectQL provides **multiple API styles** to suit different use cases: + +* **[Complete API Reference](./docs/api/README.md)** - Comprehensive guide to all API endpoints +* **[JSON-RPC API](./docs/api/README.md#json-rpc-style-api)** - Universal protocol for all operations +* **[REST API](./docs/api/README.md#rest-style-api)** - Traditional REST endpoints +* **[Metadata API](./docs/api/README.md#metadata-api)** - Runtime schema discovery and introspection +* **[Authentication Guide](./docs/api/authentication.md)** - Securing your APIs with JWT, API keys, and more + +**Quick Example:** +```bash +# Create a record via JSON-RPC +curl -X POST http://localhost:3000/api/objectql \ + -H "Content-Type: application/json" \ + -d '{ + "op": "create", + "object": "tasks", + "args": { + "name": "Complete API documentation", + "priority": "high" + } + }' +``` + +See the [API Reference](./docs/api/README.md) for complete documentation with examples. + ## 🛣 Roadmap * [ ] **Phase 1: Core Protocol:** Define stable `UnifiedQuery` types and AST parser. diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 00000000..52c343fe --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,1218 @@ +# ObjectQL API Reference + +**Version:** 1.0.0 + +This document provides a comprehensive reference for all ObjectQL API interfaces. Given the extensive metadata capabilities of ObjectQL, we provide multiple API styles to suit different use cases. + +## Table of Contents + +1. [API Overview](#api-overview) +2. [JSON-RPC Style API](#json-rpc-style-api) +3. [REST-Style API](#rest-style-api) +4. [Metadata API](#metadata-api) +5. [WebSocket API](#websocket-api) +6. [Authentication & Authorization](#authentication--authorization) +7. [Error Handling](#error-handling) +8. [Rate Limiting](#rate-limiting) +9. [Examples](#examples) + +--- + +## API Overview + +ObjectQL provides a **unified query protocol** that can be exposed through multiple API styles: + +| API Style | Use Case | Endpoint Pattern | +|-----------|----------|------------------| +| **JSON-RPC** | Universal client, AI agents, microservices | `POST /api/objectql` | +| **REST** | Traditional web apps, mobile apps | `GET/POST/PUT/DELETE /api/data/:object` | +| **Metadata** | Admin interfaces, schema discovery, runtime config | `GET /api/metadata/*` | +| **GraphQL** | Modern frontends with complex data requirements | `POST /api/graphql` *(Planned)* | +| **WebSocket** | Real-time apps, live updates | `ws://host/api/realtime` *(Planned)* | + +### Design Principles + +1. **Protocol-First**: All APIs accept/return structured JSON, never raw SQL +2. **Type-Safe**: Full TypeScript definitions for all requests/responses +3. **AI-Friendly**: Queries include optional `ai_context` for explainability +4. **Secure**: Built-in validation, permission checks, SQL injection prevention +5. **Universal**: Same query works across MongoDB, PostgreSQL, SQLite + +--- + +## JSON-RPC Style API + +The **primary ObjectQL API** is a JSON-RPC style protocol where all operations are sent to a single endpoint. + +### Base Endpoint + +``` +POST /api/objectql +Content-Type: application/json +``` + +### Request Format + +```typescript +interface ObjectQLRequest { + // Authentication context (optional, can also come from headers) + user?: { + id: string; + roles: string[]; + [key: string]: any; + }; + + // The operation to perform + op: 'find' | 'findOne' | 'create' | 'update' | 'delete' | 'count' | 'action'; + + // The target object/table + object: string; + + // Operation-specific arguments + args: any; +} +``` + +### Response Format + +```typescript +interface ObjectQLResponse { + data?: any; + error?: { + code: string; + message: string; + } +} +``` + +### Operations + +#### 1. `find` - Query Records + +Retrieve multiple records with filtering, sorting, pagination, and joins. + +**Request:** +```json +{ + "op": "find", + "object": "orders", + "args": { + "fields": ["order_no", "amount", "status", "created_at"], + "filters": [ + ["status", "=", "paid"], + "and", + ["amount", ">", 1000] + ], + "sort": [["created_at", "desc"]], + "top": 20, + "skip": 0, + "expand": { + "customer": { + "fields": ["name", "email"] + } + } + } +} +``` + +**Response:** +```json +{ + "data": [ + { + "order_no": "ORD-001", + "amount": 1500, + "status": "paid", + "created_at": "2024-01-15T10:30:00Z", + "customer": { + "name": "Acme Corp", + "email": "contact@acme.com" + } + } + ] +} +``` + +#### 2. `findOne` - Get Single Record + +Retrieve a single record by ID or query. + +**Request (by ID):** +```json +{ + "op": "findOne", + "object": "users", + "args": "user_123" +} +``` + +**Request (by query):** +```json +{ + "op": "findOne", + "object": "users", + "args": { + "filters": [["email", "=", "alice@example.com"]] + } +} +``` + +**Response:** +```json +{ + "data": { + "id": "user_123", + "name": "Alice", + "email": "alice@example.com" + } +} +``` + +#### 3. `create` - Create Record + +Insert a new record. + +**Request:** +```json +{ + "op": "create", + "object": "tasks", + "args": { + "name": "Review PR", + "priority": "high", + "assignee_id": "user_123", + "due_date": "2024-01-20" + } +} +``` + +**Response:** +```json +{ + "data": { + "id": "task_456", + "name": "Review PR", + "priority": "high", + "assignee_id": "user_123", + "due_date": "2024-01-20", + "created_at": "2024-01-15T10:30:00Z" + } +} +``` + +#### 4. `update` - Update Record + +Modify an existing record. + +**Request:** +```json +{ + "op": "update", + "object": "tasks", + "args": { + "id": "task_456", + "data": { + "status": "completed", + "completed_at": "2024-01-16T14:00:00Z" + } + } +} +``` + +**Response:** +```json +{ + "data": { + "id": "task_456", + "status": "completed", + "completed_at": "2024-01-16T14:00:00Z" + } +} +``` + +#### 5. `delete` - Delete Record + +Remove a record by ID. + +**Request:** +```json +{ + "op": "delete", + "object": "tasks", + "args": { + "id": "task_456" + } +} +``` + +**Response:** +```json +{ + "data": { + "id": "task_456", + "deleted": true + } +} +``` + +#### 6. `count` - Count Records + +Get the count of records matching a filter. + +**Request:** +```json +{ + "op": "count", + "object": "orders", + "args": { + "filters": [ + ["status", "=", "pending"] + ] + } +} +``` + +**Response:** +```json +{ + "data": 42 +} +``` + +#### 7. `action` - Execute Custom Action + +Execute a custom server-side action (RPC-style operation). + +**Request:** +```json +{ + "op": "action", + "object": "orders", + "args": { + "action": "approve", + "id": "order_789", + "input": { + "approved_by": "manager_123", + "notes": "Approved for expedited shipping" + } + } +} +``` + +**Response:** +```json +{ + "data": { + "success": true, + "message": "Order approved successfully", + "order": { + "id": "order_789", + "status": "approved", + "approved_at": "2024-01-15T10:30:00Z" + } + } +} +``` + +### Advanced Query Features + +#### AI Context (Optional) + +Add semantic information to queries for better logging, debugging, and AI processing: + +```json +{ + "op": "find", + "object": "projects", + "ai_context": { + "intent": "Find at-risk projects requiring immediate attention", + "natural_language": "Show active projects that are overdue or over budget", + "use_case": "Project manager dashboard" + }, + "args": { + "filters": [ + ["status", "=", "active"], + "and", + [ + ["end_date", "<", "$today"], + "or", + ["actual_cost", ">", "budget"] + ] + ] + } +} +``` + +#### Aggregation Queries + +Perform GROUP BY operations: + +```json +{ + "op": "find", + "object": "orders", + "args": { + "groupBy": ["category"], + "aggregate": [ + { + "func": "sum", + "field": "amount", + "alias": "total_sales" + }, + { + "func": "count", + "field": "id", + "alias": "order_count" + } + ], + "filters": [["status", "=", "paid"]], + "sort": [["total_sales", "desc"]] + } +} +``` + +--- + +## REST-Style API + +For traditional REST clients, ObjectQL can expose a REST-style interface. + +### Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/data/:object` | List records | +| `GET` | `/api/data/:object/:id` | Get single record | +| `POST` | `/api/data/:object` | Create record | +| `PUT` | `/api/data/:object/:id` | Update record | +| `DELETE` | `/api/data/:object/:id` | Delete record | + +### List Records + +```bash +GET /api/data/users?filter={"status":"active"}&sort=created_at&limit=20 +``` + +**Response:** +```json +{ + "data": [...], + "meta": { + "total": 150, + "page": 1, + "per_page": 20 + } +} +``` + +### Get Single Record + +```bash +GET /api/data/users/user_123 +``` + +**Response:** +```json +{ + "data": { + "id": "user_123", + "name": "Alice", + "email": "alice@example.com" + } +} +``` + +### Create Record + +```bash +POST /api/data/users +Content-Type: application/json + +{ + "name": "Bob", + "email": "bob@example.com", + "role": "admin" +} +``` + +**Response:** +```json +{ + "data": { + "id": "user_456", + "name": "Bob", + "email": "bob@example.com", + "role": "admin", + "created_at": "2024-01-15T10:30:00Z" + } +} +``` + +### Update Record + +```bash +PUT /api/data/users/user_456 +Content-Type: application/json + +{ + "role": "user" +} +``` + +**Response:** +```json +{ + "data": { + "id": "user_456", + "role": "user", + "updated_at": "2024-01-15T11:00:00Z" + } +} +``` + +### Delete Record + +```bash +DELETE /api/data/users/user_456 +``` + +**Response:** +```json +{ + "data": { + "id": "user_456", + "deleted": true + } +} +``` + +--- + +## Metadata API + +The Metadata API provides runtime access to schema information, object definitions, and configuration. + +### Base Endpoint + +``` +/api/metadata +``` + +### Endpoints + +#### 1. List All Objects + +Get a list of all registered objects/tables. + +```bash +GET /api/metadata/objects +``` + +**Response:** +```json +{ + "objects": [ + { + "name": "users", + "label": "Users", + "icon": "user", + "description": "System users and authentication", + "fields": {...} + }, + { + "name": "orders", + "label": "Orders", + "icon": "shopping-cart", + "description": "Customer orders", + "fields": {...} + } + ] +} +``` + +#### 2. Get Object Schema + +Get detailed schema for a specific object. + +```bash +GET /api/metadata/objects/users +``` + +**Response:** +```json +{ + "name": "users", + "label": "Users", + "icon": "user", + "description": "System users and authentication", + "fields": [ + { + "name": "email", + "type": "email", + "label": "Email Address", + "required": true, + "unique": true + }, + { + "name": "role", + "type": "select", + "label": "Role", + "options": ["admin", "user", "guest"], + "defaultValue": "user" + } + ], + "actions": [ + { + "name": "reset_password", + "type": "record", + "label": "Reset Password" + } + ], + "hooks": [ + { + "event": "afterCreate", + "description": "Send welcome email" + } + ] +} +``` + +#### 3. Update Metadata (Admin) + +Dynamically update object configuration at runtime. + +```bash +PUT /api/metadata/object/users +Content-Type: application/json +Authorization: Bearer + +{ + "label": "System Users", + "description": "Updated description" +} +``` + +**Response:** +```json +{ + "success": true +} +``` + +#### 4. Get Field Metadata + +Get detailed information about a specific field. + +```bash +GET /api/metadata/objects/users/fields/email +``` + +**Response:** +```json +{ + "name": "email", + "type": "email", + "label": "Email Address", + "required": true, + "unique": true, + "validations": [ + { + "type": "email_format", + "message": "Must be a valid email address" + } + ] +} +``` + +#### 5. List Actions + +Get all custom actions for an object. + +```bash +GET /api/metadata/objects/orders/actions +``` + +**Response:** +```json +{ + "actions": [ + { + "name": "approve", + "type": "record", + "label": "Approve Order", + "params": { + "notes": { + "type": "textarea", + "label": "Approval Notes" + } + } + }, + { + "name": "bulk_import", + "type": "global", + "label": "Bulk Import Orders" + } + ] +} +``` + +--- + +## WebSocket API + +*(Planned Feature)* + +For real-time updates and live data synchronization. + +### Connection + +```javascript +const ws = new WebSocket('ws://localhost:3000/api/realtime'); + +ws.onopen = () => { + // Authenticate + ws.send(JSON.stringify({ + type: 'auth', + token: 'your_jwt_token' + })); + + // Subscribe to changes + ws.send(JSON.stringify({ + type: 'subscribe', + object: 'orders', + filters: [["status", "=", "pending"]] + })); +}; + +ws.onmessage = (event) => { + const data = JSON.parse(event.data); + + if (data.type === 'change') { + console.log('Record changed:', data.record); + } +}; +``` + +--- + +## Authentication & Authorization + +### Authentication Methods + +ObjectQL supports multiple authentication strategies: + +#### 1. JWT Tokens (Recommended) + +```bash +POST /api/objectql +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... +Content-Type: application/json +``` + +#### 2. API Keys + +```bash +POST /api/objectql +X-API-Key: your_api_key_here +Content-Type: application/json +``` + +#### 3. Session Cookies + +```bash +POST /api/objectql +Cookie: session_id=abc123... +Content-Type: application/json +``` + +#### 4. User Context in Request (Development Only) + +For testing and development, you can pass user context directly in the request: + +```json +{ + "user": { + "id": "user_123", + "roles": ["admin"] + }, + "op": "find", + "object": "users", + "args": {} +} +``` + +⚠️ **Warning**: In production, always authenticate via headers, not request body. + +### Permission System + +ObjectQL enforces permissions at multiple levels: + +1. **Object-Level**: Can the user access this object at all? +2. **Operation-Level**: Can they perform this operation (read/create/update/delete)? +3. **Field-Level**: Which fields can they see/edit? +4. **Record-Level**: Which specific records can they access? + +**Permission Check Flow:** +``` +Request → Authentication → Object Permission → Field Permission → Record Permission → Execute +``` + +**Example Permission Config:** +```yaml +# user.object.yml +permissions: + - profile: admin + allow_read: true + allow_create: true + allow_edit: true + allow_delete: true + + - profile: user + allow_read: true + allow_create: false + allow_edit: true + allow_delete: false + record_filters: + - ["owner", "=", "$current_user"] +``` + +--- + +## Error Handling + +### Error Response Format + +All errors follow a consistent format: + +```json +{ + "error": { + "code": "ERROR_CODE", + "message": "Human-readable error message", + "details": { + "field": "email", + "reason": "Email already exists" + } + } +} +``` + +### Error Codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `INVALID_REQUEST` | 400 | Malformed request body | +| `VALIDATION_ERROR` | 400 | Data validation failed | +| `UNAUTHORIZED` | 401 | Authentication required | +| `FORBIDDEN` | 403 | Insufficient permissions | +| `NOT_FOUND` | 404 | Object or record not found | +| `CONFLICT` | 409 | Unique constraint violation | +| `INTERNAL_ERROR` | 500 | Server error | +| `DATABASE_ERROR` | 500 | Database operation failed | + +### Example Error Responses + +**Validation Error:** +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Validation failed", + "details": { + "fields": { + "email": "Invalid email format", + "age": "Must be greater than 0" + } + } + } +} +``` + +**Permission Error:** +```json +{ + "error": { + "code": "FORBIDDEN", + "message": "You do not have permission to access this resource", + "details": { + "required_permission": "users:delete", + "user_roles": ["user"] + } + } +} +``` + +**Not Found:** +```json +{ + "error": { + "code": "NOT_FOUND", + "message": "Object 'xyz' not found" + } +} +``` + +--- + +## Rate Limiting + +ObjectQL supports configurable rate limiting to prevent abuse. + +### Default Limits + +| Tier | Requests/Minute | Requests/Hour | +|------|-----------------|---------------| +| Anonymous | 20 | 100 | +| Authenticated | 100 | 1000 | +| Premium | 500 | 10000 | + +### Rate Limit Headers + +All responses include rate limit information: + +``` +X-RateLimit-Limit: 100 +X-RateLimit-Remaining: 95 +X-RateLimit-Reset: 1642258800 +``` + +### Rate Limit Exceeded + +When rate limit is exceeded: + +```json +{ + "error": { + "code": "RATE_LIMIT_EXCEEDED", + "message": "Too many requests. Please try again later.", + "details": { + "retry_after": 60 + } + } +} +``` + +**HTTP Status**: `429 Too Many Requests` + +--- + +## Examples + +### Example 1: User Registration Flow + +```javascript +// 1. Create user +const response = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'users', + args: { + email: 'alice@example.com', + name: 'Alice', + password_hash: 'hashed_password' + } + }) +}); + +const { data: user } = await response.json(); +// { id: 'user_123', email: 'alice@example.com', ... } + +// 2. Send verification email (triggered by hook) +// 3. User verifies email via action +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'action', + object: 'users', + args: { + action: 'verify_email', + id: user.id, + input: { + token: 'verification_token_xyz' + } + } + }) +}); +``` + +### Example 2: Dashboard Analytics + +```javascript +// Get sales metrics for dashboard +const response = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + jwt_token + }, + body: JSON.stringify({ + op: 'find', + object: 'orders', + ai_context: { + intent: "Calculate monthly sales by category", + use_case: "Executive dashboard" + }, + args: { + groupBy: ['category', 'month'], + aggregate: [ + { func: 'sum', field: 'amount', alias: 'revenue' }, + { func: 'count', field: 'id', alias: 'order_count' }, + { func: 'avg', field: 'amount', alias: 'avg_order_value' } + ], + filters: [ + ['status', '=', 'paid'], + 'and', + ['created_at', '>=', '2024-01-01'] + ], + sort: [['month', 'asc'], ['revenue', 'desc']] + } + }) +}); + +const { data } = await response.json(); +// [ +// { category: 'Electronics', month: '2024-01', revenue: 50000, order_count: 120, avg_order_value: 416.67 }, +// { category: 'Clothing', month: '2024-01', revenue: 30000, order_count: 250, avg_order_value: 120.00 }, +// ... +// ] +``` + +### Example 3: Complex Search with Relations + +```javascript +// Find customers with high-value recent orders +const response = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + jwt_token + }, + body: JSON.stringify({ + op: 'find', + object: 'customers', + args: { + fields: ['name', 'email', 'vip_level', 'total_spent'], + filters: [ + ['vip_level', '>=', 'gold'], + 'and', + ['is_active', '=', true] + ], + expand: { + orders: { + fields: ['order_no', 'amount', 'status'], + filters: [ + ['created_at', '>', '2024-01-01'], + 'and', + ['amount', '>', 1000] + ], + sort: [['created_at', 'desc']], + top: 5 + } + }, + sort: [['total_spent', 'desc']], + top: 20 + } + }) +}); +``` + +### Example 4: Bulk Operations + +```javascript +// Create multiple records in one request +const response = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'action', + object: 'tasks', + args: { + action: 'bulk_create', + input: { + items: [ + { name: 'Task 1', priority: 'high' }, + { name: 'Task 2', priority: 'medium' }, + { name: 'Task 3', priority: 'low' } + ] + } + } + }) +}); +``` + +### Example 5: Metadata-Driven Form Generation + +```javascript +// 1. Fetch object schema +const schemaResponse = await fetch('/api/metadata/objects/contacts'); +const schema = await schemaResponse.json(); + +// 2. Generate form based on field metadata +const form = generateForm(schema.fields); + +// 3. Submit form data +const submitResponse = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'contacts', + args: formData + }) +}); +``` + +--- + +## OpenAPI/Swagger Specification + +ObjectQL automatically generates OpenAPI 3.0 specifications from your metadata. + +### Access the Spec + +```bash +GET /api/objectql/openapi.json +``` + +This can be imported into: +- **Swagger UI** for interactive API documentation +- **Postman** for testing +- **Code Generators** for client SDKs + +### Example Integration + +```html + + + + + + + +
+ + + + +``` + +--- + +## Best Practices + +### 1. Use Specific Field Projections + +❌ **Don't** fetch all fields: +```json +{ + "op": "find", + "object": "users" +} +``` + +✅ **Do** specify only needed fields: +```json +{ + "op": "find", + "object": "users", + "args": { + "fields": ["id", "name", "email"] + } +} +``` + +### 2. Use Pagination for Large Datasets + +```json +{ + "op": "find", + "object": "orders", + "args": { + "top": 50, + "skip": 0 + } +} +``` + +### 3. Add Indexes for Filtered Fields + +If you frequently filter by a field, add an index: + +```yaml +# order.object.yml +indexes: + - fields: [status, created_at] +``` + +### 4. Use AI Context for Complex Queries + +```json +{ + "op": "find", + "object": "projects", + "ai_context": { + "intent": "Find at-risk projects", + "natural_language": "Active projects that are overdue or over budget" + }, + "args": {...} +} +``` + +### 5. Batch Related Requests + +Use `expand` instead of multiple requests: + +❌ **Don't**: +```javascript +// Multiple requests +const orders = await getOrders(); +for (const order of orders) { + order.customer = await getCustomer(order.customer_id); +} +``` + +✅ **Do**: +```json +{ + "op": "find", + "object": "orders", + "args": { + "expand": { + "customer": { + "fields": ["name", "email"] + } + } + } +} +``` + +--- + +## Next Steps + +- **[Query Language Specification](../spec/query-language.md)** - Deep dive into filter syntax +- **[Actions Guide](../guide/logic-actions.md)** - Building custom RPC operations +- **[Server Integration](../guide/server-integration.md)** - Deploying ObjectQL APIs +- **[Authentication Guide](../guide/authentication.md)** *(Coming Soon)* +- **[GraphQL API](./graphql.md)** *(Planned)* + +--- + +## Support + +- **GitHub Issues**: [objectql/objectql/issues](https://github.com/objectql/objectql/issues) +- **Documentation**: [objectql.org](https://objectql.org) +- **Community**: [Discord](https://discord.gg/objectql) + +--- + +**Last Updated**: January 2024 +**API Version**: 1.0.0 diff --git a/docs/api/authentication.md b/docs/api/authentication.md new file mode 100644 index 00000000..f7712e70 --- /dev/null +++ b/docs/api/authentication.md @@ -0,0 +1,624 @@ +# Authentication & Authorization Guide + +This guide covers how to secure your ObjectQL APIs with authentication and implement fine-grained authorization. + +## Table of Contents + +1. [Authentication Strategies](#authentication-strategies) +2. [Authorization & Permissions](#authorization--permissions) +3. [User Context](#user-context) +4. [Security Best Practices](#security-best-practices) + +--- + +## Authentication Strategies + +ObjectQL is designed to integrate with any authentication system. The framework is **authentication-agnostic** - you bring your own auth provider. + +### 1. JWT (JSON Web Tokens) + +**Recommended for:** Modern web apps, mobile apps, microservices + +#### Setup + +```typescript +import { ObjectQL } from '@objectql/core'; +import { createNodeHandler } from '@objectql/server'; +import express from 'express'; +import jwt from 'jsonwebtoken'; + +const app = new ObjectQL({ /* config */ }); +const server = express(); + +// Middleware to verify JWT and attach user context +server.use('/api/objectql', async (req, res, next) => { + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'No token provided' }}); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = decoded; // Attach user to request + next(); + } catch (e) { + return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'Invalid token' }}); + } +}); + +// ObjectQL handler will read req.user automatically +server.all('/api/objectql', createNodeHandler(app)); +``` + +#### Client Usage + +```javascript +const response = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + op: 'find', + object: 'users', + args: {} + }) +}); +``` + +### 2. API Keys + +**Recommended for:** Server-to-server communication, integrations, scripts + +#### Setup + +```typescript +const API_KEYS = { + 'key_abc123': { id: 'service_account_1', roles: ['api'] }, + 'key_xyz789': { id: 'service_account_2', roles: ['readonly'] } +}; + +server.use('/api/objectql', (req, res, next) => { + const apiKey = req.headers['x-api-key']; + + if (!apiKey) { + return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'API key required' }}); + } + + const user = API_KEYS[apiKey]; + if (!user) { + return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'Invalid API key' }}); + } + + req.user = user; + next(); +}); +``` + +#### Client Usage + +```bash +curl -X POST http://localhost:3000/api/objectql \ + -H "X-API-Key: key_abc123" \ + -H "Content-Type: application/json" \ + -d '{"op":"find","object":"users","args":{}}' +``` + +### 3. Session Cookies + +**Recommended for:** Traditional web applications + +#### Setup + +```typescript +import session from 'express-session'; + +server.use(session({ + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: false +})); + +server.use('/api/objectql', (req, res, next) => { + if (!req.session.user) { + return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'Not logged in' }}); + } + + req.user = req.session.user; + next(); +}); +``` + +### 4. OAuth2 / OpenID Connect + +**Recommended for:** Enterprise SSO, social login + +#### Setup with Passport.js + +```typescript +import passport from 'passport'; +import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; + +passport.use(new GoogleStrategy({ + clientID: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + callbackURL: '/auth/google/callback' +}, (accessToken, refreshToken, profile, done) => { + // Find or create user + const user = { id: profile.id, email: profile.emails[0].value }; + done(null, user); +})); + +server.use(passport.initialize()); +server.use(passport.session()); + +server.use('/api/objectql', (req, res, next) => { + if (!req.isAuthenticated()) { + return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'Authentication required' }}); + } + + req.user = req.user; + next(); +}); +``` + +--- + +## Authorization & Permissions + +Once authenticated, ObjectQL enforces **multi-level authorization**: + +### Permission Levels + +1. **Object-Level**: Can user access this table at all? +2. **Operation-Level**: Can they read/create/update/delete? +3. **Field-Level**: Which fields can they see/modify? +4. **Record-Level**: Which specific records can they access? + +### Defining Permissions + +Permissions are defined in the object metadata: + +```yaml +# user.object.yml +name: user +fields: + email: { type: email } + role: { type: select, options: [admin, user] } + salary: { type: number } + +permissions: + # Admin: Full access + - profile: admin + allow_read: true + allow_create: true + allow_edit: true + allow_delete: true + + # Regular users: Limited access + - profile: user + allow_read: true + allow_create: false + allow_edit: true + allow_delete: false + + # Can only see/edit own record + record_filters: + - ["id", "=", "$current_user"] + + # Cannot see salary field + field_permissions: + salary: + visible: false + editable: false +``` + +### Permission Profiles + +Define reusable permission profiles: + +```yaml +# profiles.yml +profiles: + admin: + label: Administrator + description: Full system access + + user: + label: Standard User + description: Limited access to own data + + guest: + label: Guest + description: Read-only access +``` + +### Dynamic Record Filters + +Use special variables in filters: + +| Variable | Description | +|----------|-------------| +| `$current_user` | ID of authenticated user | +| `$current_user.role` | User's role | +| `$current_user.department` | User's department | +| `$today` | Current date | +| `$now` | Current timestamp | + +**Example:** +```yaml +permissions: + - profile: manager + allow_read: true + record_filters: + # Managers can only see their department's data + - ["department", "=", "$current_user.department"] +``` + +### Field-Level Permissions + +Control which fields are visible/editable: + +```yaml +permissions: + - profile: user + allow_edit: true + field_permissions: + # Can view but not edit + created_at: { visible: true, editable: false } + created_by: { visible: true, editable: false } + + # Cannot see at all + internal_notes: { visible: false, editable: false } + + # Conditional visibility + salary: + visible: "record.owner == $current_user || $current_user.role == 'hr'" +``` + +### Operation-Level Permissions + +Different permissions for different operations: + +```yaml +permissions: + - profile: auditor + allow_read: true # Can view + allow_create: false # Cannot create + allow_edit: false # Cannot edit + allow_delete: false # Cannot delete + + - profile: contributor + allow_read: true + allow_create: true # Can create + allow_edit: true # Can edit own records + allow_delete: false # Cannot delete + record_filters: + - ["owner", "=", "$current_user"] +``` + +--- + +## User Context + +### Accessing User Context in Code + +In hooks and actions, access the authenticated user: + +```typescript +// In a hook +export const beforeCreate = async (ctx: HookContext) => { + // Automatically set owner to current user + ctx.doc.owner = ctx.user.id; + ctx.doc.created_by = ctx.user.id; +}; +``` + +```typescript +// In an action +export const approveOrder: ActionHandler = async (ctx) => { + // Check if user is a manager + if (!ctx.user.roles.includes('manager')) { + throw new Error('Only managers can approve orders'); + } + + // Log who approved + await ctx.api.object('audit_logs').create({ + action: 'approve_order', + order_id: ctx.id, + approved_by: ctx.user.id + }); +}; +``` + +### User Context Structure + +```typescript +interface UserContext { + id: string | number; + roles?: string[]; + permissions?: string[]; + [key: string]: any; // Custom properties +} +``` + +### Custom User Properties + +You can add any properties to the user context: + +```typescript +server.use('/api/objectql', (req, res, next) => { + req.user = { + id: decoded.userId, + roles: decoded.roles, + department: decoded.department, + tenant_id: decoded.tenantId, // For multi-tenancy + preferences: decoded.preferences + }; + next(); +}); +``` + +--- + +## Security Best Practices + +### 1. Always Validate Permissions Server-Side + +❌ **Never** rely on client-side permission checks: +```javascript +// Client side - NOT SECURE +if (user.isAdmin) { + deleteUser(userId); // Client can fake this! +} +``` + +✅ **Always** enforce on server: +```yaml +# server-side permissions +permissions: + - profile: admin + allow_delete: true + - profile: user + allow_delete: false +``` + +### 2. Use Least Privilege + +Give users only the permissions they need: + +```yaml +# Start restrictive +permissions: + - profile: default + allow_read: false + allow_create: false + allow_edit: false + allow_delete: false + +# Then grant specific access + - profile: viewer + allow_read: true +``` + +### 3. Audit Sensitive Operations + +Log all privileged actions: + +```typescript +// Hook: afterDelete +export const afterDelete = async (ctx: HookContext) => { + await ctx.api.object('audit_logs').create({ + action: 'delete', + object: ctx.objectName, + record_id: ctx.id, + user_id: ctx.user.id, + timestamp: new Date() + }); +}; +``` + +### 4. Implement Rate Limiting + +Prevent brute force attacks: + +```typescript +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100 // limit each IP to 100 requests per windowMs +}); + +server.use('/api/objectql', limiter); +``` + +### 5. Sanitize User Input + +Always validate and sanitize: + +```yaml +# Use validation rules +validations: + - field: email + type: email + - field: age + type: number + min: 0 + max: 150 +``` + +### 6. Use HTTPS in Production + +```typescript +// Force HTTPS +server.use((req, res, next) => { + if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') { + res.redirect(`https://${req.header('host')}${req.url}`); + } else { + next(); + } +}); +``` + +### 7. Implement CORS Properly + +```typescript +import cors from 'cors'; + +// Development +server.use(cors({ + origin: 'http://localhost:3000', + credentials: true +})); + +// Production +server.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(','), + credentials: true +})); +``` + +### 8. Rotate Secrets Regularly + +- JWT secrets +- API keys +- Database passwords +- Session secrets + +### 9. Multi-Tenancy + +Isolate data between tenants: + +```typescript +// Add tenant_id to all requests +server.use('/api/objectql', (req, res, next) => { + req.user = { + ...decoded, + tenant_id: decoded.tenantId + }; + next(); +}); +``` + +```yaml +# Enforce tenant isolation +permissions: + - profile: user + allow_read: true + record_filters: + - ["tenant_id", "=", "$current_user.tenant_id"] +``` + +### 10. Security Headers + +```typescript +import helmet from 'helmet'; + +server.use(helmet()); +``` + +--- + +## Example: Complete Auth Setup + +```typescript +import express from 'express'; +import { ObjectQL } from '@objectql/core'; +import { createNodeHandler } from '@objectql/server'; +import jwt from 'jsonwebtoken'; +import rateLimit from 'express-rate-limit'; +import helmet from 'helmet'; +import cors from 'cors'; + +const app = new ObjectQL({ /* config */ }); +const server = express(); + +// 1. Security headers +server.use(helmet()); + +// 2. CORS +server.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(','), + credentials: true +})); + +// 3. Rate limiting +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100 +}); +server.use('/api', limiter); + +// 4. Authentication middleware +const authenticate = async (req, res, next) => { + try { + const token = req.headers.authorization?.replace('Bearer ', ''); + + if (!token) { + return res.status(401).json({ + error: { code: 'UNAUTHORIZED', message: 'No token provided' } + }); + } + + const decoded = jwt.verify(token, process.env.JWT_SECRET); + + // Fetch full user context from database if needed + const userContext = await app.getContext().object('users').findOne(decoded.userId); + + req.user = { + id: userContext.id, + roles: userContext.roles, + department: userContext.department, + tenant_id: userContext.tenant_id + }; + + next(); + } catch (e) { + return res.status(401).json({ + error: { code: 'UNAUTHORIZED', message: 'Invalid token' } + }); + } +}; + +// 5. Apply authentication +server.use('/api/objectql', authenticate); + +// 6. Mount ObjectQL handler +server.all('/api/objectql', createNodeHandler(app)); + +// 7. Public login endpoint (no auth required) +server.post('/api/auth/login', async (req, res) => { + const { email, password } = req.body; + + // Verify credentials (example - use proper password hashing!) + const user = await verifyCredentials(email, password); + + if (!user) { + return res.status(401).json({ + error: { code: 'INVALID_CREDENTIALS', message: 'Invalid email or password' } + }); + } + + // Generate JWT + const token = jwt.sign( + { userId: user.id }, + process.env.JWT_SECRET, + { expiresIn: '7d' } + ); + + res.json({ token, user }); +}); + +server.listen(3000); +``` + +--- + +## Next Steps + +- [Permissions Specification](../spec/permission.md) +- [Actions Guide](../guide/logic-actions.md) +- [Multi-Tenancy Guide](../guide/multi-tenancy.md) *(Coming Soon)* diff --git a/docs/guide/index.md b/docs/guide/index.md index 53fcf5ea..351f39d0 100644 --- a/docs/guide/index.md +++ b/docs/guide/index.md @@ -52,5 +52,18 @@ Your application logic isn't hidden inside controller functions. It's attached t * **For Low-Code**: The rigorous structure makes it easy to build UI visual editors on top of it. * **For Microservices**: The federation protocol allows you to stitch multiple services into one graph instantly. +## 📡 API Access -* [**CLI Tool**](./cli.md): Using the command line interface for codegen. +ObjectQL exposes your data through multiple API styles: + +* **[Complete API Reference](../api/README.md)** - Comprehensive guide to all endpoints +* **[JSON-RPC API](../api/README.md#json-rpc-style-api)** - Universal protocol for all operations +* **[REST API](../api/README.md#rest-style-api)** - Traditional REST endpoints +* **[Metadata API](../api/README.md#metadata-api)** - Runtime schema discovery +* **[Authentication Guide](../api/authentication.md)** - Securing your APIs + +## Next Steps + +* [**Getting Started**](./getting-started.md): Build your first ObjectQL app +* [**CLI Tool**](./cli.md): Using the command line interface for codegen +* [**API Reference**](../api/README.md): Complete API documentation From bfca6fdb277bdd92450dc9f0f2724ed25479d99c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:24:03 +0000 Subject: [PATCH 3/8] Add API index and quick reference guide Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- docs/api/README.md | 20 +- docs/api/index.md | 143 +++++++++++ docs/api/quick-reference.md | 496 ++++++++++++++++++++++++++++++++++++ 3 files changed, 656 insertions(+), 3 deletions(-) create mode 100644 docs/api/index.md create mode 100644 docs/api/quick-reference.md diff --git a/docs/api/README.md b/docs/api/README.md index 52c343fe..916682af 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -174,14 +174,28 @@ Insert a new record. **Request:** ```json +{ + "op": "create", + "object": "tasks", + "args": { + "data": { + "name": "Review PR", + "priority": "high", + "assignee_id": "user_123", + "due_date": "2024-01-20" + } + } +} +``` + +**Note**: Alternatively, you can pass the document directly as `args` without the `data` wrapper: +```json { "op": "create", "object": "tasks", "args": { "name": "Review PR", - "priority": "high", - "assignee_id": "user_123", - "due_date": "2024-01-20" + "priority": "high" } } ``` diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 00000000..d70570de --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,143 @@ +# API Documentation + +Welcome to the ObjectQL API Reference. + +## Quick Links + +### Main Documentation +- **[Complete API Reference](./README.md)** - Comprehensive guide to all API endpoints and features + +### Authentication & Security +- **[Authentication Guide](./authentication.md)** - JWT, API keys, OAuth2, and more +- [Rate Limiting](./README.md#rate-limiting) +- [Error Handling](./README.md#error-handling) + +### API Styles + +ObjectQL supports multiple API styles to fit your use case: + +#### 1. JSON-RPC API (Primary) +- **Endpoint**: `POST /api/objectql` +- **Use Case**: Universal client, AI agents, microservices +- [Documentation](./README.md#json-rpc-style-api) + +**Operations:** +- `find` - Query multiple records +- `findOne` - Get single record +- `create` - Insert new record +- `update` - Modify existing record +- `delete` - Remove record +- `count` - Count records +- `action` - Execute custom server-side operation + +#### 2. REST API +- **Endpoints**: `GET/POST/PUT/DELETE /api/data/:object` +- **Use Case**: Traditional web apps, mobile apps +- [Documentation](./README.md#rest-style-api) + +#### 3. Metadata API +- **Endpoints**: `GET /api/metadata/*` +- **Use Case**: Admin interfaces, schema discovery, runtime introspection +- [Documentation](./README.md#metadata-api) + +**Endpoints:** +- `GET /api/metadata/objects` - List all objects +- `GET /api/metadata/objects/:name` - Get object schema +- `GET /api/metadata/objects/:name/fields/:field` - Get field metadata +- `GET /api/metadata/objects/:name/actions` - List custom actions + +#### 4. WebSocket API (Planned) +- **Endpoint**: `ws://host/api/realtime` +- **Use Case**: Real-time apps, live updates +- [Documentation](./README.md#websocket-api) + +### Additional Resources + +- [OpenAPI/Swagger Spec](./README.md#openapiswagger-specification) - Auto-generated API specification +- [Best Practices](./README.md#best-practices) - Optimization tips +- [Examples](./README.md#examples) - Complete code examples + +## Quick Start + +### Basic Query Example + +```bash +curl -X POST http://localhost:3000/api/objectql \ + -H "Content-Type: application/json" \ + -d '{ + "op": "find", + "object": "users", + "args": { + "fields": ["id", "name", "email"], + "filters": [["is_active", "=", true]], + "top": 10 + } + }' +``` + +### Create Record Example + +```bash +curl -X POST http://localhost:3000/api/objectql \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -d '{ + "op": "create", + "object": "tasks", + "args": { + "data": { + "name": "Complete documentation", + "priority": "high", + "due_date": "2024-01-20" + } + } + }' +``` + +### Get Metadata Example + +```bash +curl http://localhost:3000/api/metadata/objects/users +``` + +## Response Format + +All APIs return consistent JSON responses: + +**Success:** +```json +{ + "data": { + // Your data here + } +} +``` + +**Error:** +```json +{ + "error": { + "code": "ERROR_CODE", + "message": "Human-readable error message" + } +} +``` + +## Related Documentation + +- [Query Language Specification](../spec/query-language.md) - Deep dive into filter syntax +- [Actions Guide](../guide/logic-actions.md) - Building custom operations +- [Hooks Guide](../guide/logic-hooks.md) - Event-driven logic +- [Server Integration](../guide/server-integration.md) - Deploying ObjectQL +- [Permissions](../spec/permission.md) - Access control + +## Support + +- **Issues**: [GitHub Issues](https://github.com/objectql/objectql/issues) +- **Documentation**: [Main Docs](../index.md) +- **Examples**: [Examples Directory](../../examples/) + +--- + +**Last Updated**: January 2024 +**API Version**: 1.0.0 diff --git a/docs/api/quick-reference.md b/docs/api/quick-reference.md new file mode 100644 index 00000000..a94d72b8 --- /dev/null +++ b/docs/api/quick-reference.md @@ -0,0 +1,496 @@ +# API Quick Reference + +This is a condensed reference for the most common ObjectQL API operations. + +## Base Endpoint + +``` +POST /api/objectql +Content-Type: application/json +Authorization: Bearer (optional) +``` + +## Common Operations + +### 📋 List Records + +```json +{ + "op": "find", + "object": "users", + "args": { + "fields": ["id", "name", "email"], + "top": 20, + "skip": 0 + } +} +``` + +### 🔍 Search Records + +```json +{ + "op": "find", + "object": "products", + "args": { + "fields": ["id", "name", "price"], + "filters": [ + ["category", "=", "electronics"], + "and", + ["price", "<", 1000] + ], + "sort": [["price", "asc"]] + } +} +``` + +### 👤 Get Single Record + +```json +{ + "op": "findOne", + "object": "users", + "args": "user_123" +} +``` + +or with filters: + +```json +{ + "op": "findOne", + "object": "users", + "args": { + "filters": [["email", "=", "alice@example.com"]] + } +} +``` + +### ➕ Create Record + +```json +{ + "op": "create", + "object": "tasks", + "args": { + "data": { + "name": "Complete documentation", + "priority": "high", + "due_date": "2024-01-20" + } + } +} +``` + +### ✏️ Update Record + +```json +{ + "op": "update", + "object": "tasks", + "args": { + "id": "task_456", + "data": { + "status": "completed" + } + } +} +``` + +### ❌ Delete Record + +```json +{ + "op": "delete", + "object": "tasks", + "args": { + "id": "task_456" + } +} +``` + +### 🔢 Count Records + +```json +{ + "op": "count", + "object": "orders", + "args": { + "filters": [["status", "=", "pending"]] + } +} +``` + +### ⚡ Execute Action + +```json +{ + "op": "action", + "object": "orders", + "args": { + "action": "approve", + "id": "order_789", + "input": { + "notes": "Approved for expedited shipping" + } + } +} +``` + +## Filter Operators + +| Operator | Description | Example | +|----------|-------------|---------| +| `=` | Equal | `["status", "=", "active"]` | +| `!=` | Not equal | `["status", "!=", "deleted"]` | +| `>` | Greater than | `["age", ">", 18]` | +| `>=` | Greater or equal | `["price", ">=", 100]` | +| `<` | Less than | `["stock", "<", 10]` | +| `<=` | Less or equal | `["rating", "<=", 3]` | +| `in` | In array | `["status", "in", ["pending", "active"]]` | +| `not in` | Not in array | `["status", "not in", ["deleted", "archived"]]` | +| `like` | SQL LIKE pattern | `["name", "like", "%john%"]` | +| `startswith` | Starts with | `["email", "startswith", "admin"]` | +| `endswith` | Ends with | `["domain", "endswith", ".com"]` | +| `contains` | Contains substring | `["tags", "contains", "urgent"]` | +| `between` | Between range | `["price", "between", [100, 500]]` | + +## Combining Filters + +### AND Condition + +```json +{ + "filters": [ + ["status", "=", "active"], + "and", + ["age", ">", 18] + ] +} +``` + +### OR Condition + +```json +{ + "filters": [ + ["priority", "=", "high"], + "or", + ["urgent", "=", true] + ] +} +``` + +### Complex Nested Logic + +```json +{ + "filters": [ + ["status", "=", "active"], + "and", + [ + ["priority", "=", "high"], + "or", + ["overdue", "=", true] + ] + ] +} +``` + +## Pagination + +```json +{ + "op": "find", + "object": "posts", + "args": { + "top": 20, // Page size + "skip": 40, // Skip first 40 (page 3) + "sort": [["created_at", "desc"]] + } +} +``` + +## Sorting + +### Single Field + +```json +{ + "sort": [["created_at", "desc"]] +} +``` + +### Multiple Fields + +```json +{ + "sort": [ + ["priority", "desc"], + ["created_at", "asc"] + ] +} +``` + +## Field Selection + +### Specific Fields Only + +```json +{ + "fields": ["id", "name", "email"] +} +``` + +### All Fields (Default) + +```json +{ + "fields": null // or omit the fields property +} +``` + +## Relationships (Expand/Join) + +### Basic Expand + +```json +{ + "op": "find", + "object": "orders", + "args": { + "fields": ["id", "order_no", "amount"], + "expand": { + "customer": { + "fields": ["name", "email"] + } + } + } +} +``` + +### Nested Expand + +```json +{ + "expand": { + "customer": { + "fields": ["name", "email"], + "expand": { + "company": { + "fields": ["name", "industry"] + } + } + } + } +} +``` + +### Expand with Filters + +```json +{ + "expand": { + "orders": { + "fields": ["order_no", "amount"], + "filters": [["status", "=", "paid"]], + "sort": [["created_at", "desc"]], + "top": 5 + } + } +} +``` + +## Aggregation + +```json +{ + "op": "find", + "object": "sales", + "args": { + "groupBy": ["category"], + "aggregate": [ + { + "func": "sum", + "field": "amount", + "alias": "total_sales" + }, + { + "func": "count", + "field": "id", + "alias": "num_orders" + }, + { + "func": "avg", + "field": "amount", + "alias": "avg_order_value" + } + ], + "sort": [["total_sales", "desc"]] + } +} +``` + +### Aggregation Functions + +- `count` - Count records +- `sum` - Sum values +- `avg` - Average value +- `min` - Minimum value +- `max` - Maximum value + +## Error Handling + +### Validation Error Response + +```json +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Validation failed", + "details": { + "fields": { + "email": "Invalid email format", + "age": "Must be greater than 0" + } + } + } +} +``` + +### Permission Error Response + +```json +{ + "error": { + "code": "FORBIDDEN", + "message": "You do not have permission to access this resource" + } +} +``` + +### Not Found Response + +```json +{ + "error": { + "code": "NOT_FOUND", + "message": "Record not found" + } +} +``` + +## Special Variables + +Use these in filters for dynamic values: + +| Variable | Description | +|----------|-------------| +| `$current_user` | Current user's ID | +| `$current_user.role` | Current user's role | +| `$today` | Current date | +| `$now` | Current timestamp | + +**Example:** +```json +{ + "filters": [ + ["owner", "=", "$current_user"], + "and", + ["due_date", "<", "$today"] + ] +} +``` + +## REST-Style Endpoints + +### List + +```bash +GET /api/data/users?top=20&skip=0 +``` + +### Get One + +```bash +GET /api/data/users/user_123 +``` + +### Create + +```bash +POST /api/data/users +Content-Type: application/json + +{"name": "Alice", "email": "alice@example.com"} +``` + +### Update + +```bash +PUT /api/data/users/user_123 +Content-Type: application/json + +{"role": "admin"} +``` + +### Delete + +```bash +DELETE /api/data/users/user_123 +``` + +## Metadata Endpoints + +### List All Objects + +```bash +GET /api/metadata/objects +``` + +### Get Object Schema + +```bash +GET /api/metadata/objects/users +``` + +### Get Field Metadata + +```bash +GET /api/metadata/objects/users/fields/email +``` + +### List Actions + +```bash +GET /api/metadata/objects/orders/actions +``` + +## Tips & Best Practices + +### ✅ DO + +- Always specify `fields` to reduce payload size +- Use pagination for large datasets +- Add indexes for frequently filtered fields +- Use `expand` instead of multiple requests +- Include authentication tokens + +### ❌ DON'T + +- Fetch all fields when you only need a few +- Query without pagination on large tables +- Make multiple requests when you can use `expand` +- Expose sensitive fields to unauthorized users +- Use raw SQL (ObjectQL prevents this by design) + +## Next Steps + +- [Complete API Reference](./README.md) +- [Authentication Guide](./authentication.md) +- [Query Language Spec](../spec/query-language.md) +- [Examples](./README.md#examples) + +--- + +**Quick Tip**: All examples use JSON-RPC format at `POST /api/objectql`. For REST endpoints, adapt to `GET/POST/PUT/DELETE /api/data/:object`. From a7b4242095bd82bfa61c1ea8cc282a6fb019de5f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:25:52 +0000 Subject: [PATCH 4/8] Fix API documentation issues from code review Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- docs/api/README.md | 22 ++++------------------ docs/api/authentication.md | 3 ++- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/docs/api/README.md b/docs/api/README.md index 916682af..871aaf20 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -174,28 +174,14 @@ Insert a new record. **Request:** ```json -{ - "op": "create", - "object": "tasks", - "args": { - "data": { - "name": "Review PR", - "priority": "high", - "assignee_id": "user_123", - "due_date": "2024-01-20" - } - } -} -``` - -**Note**: Alternatively, you can pass the document directly as `args` without the `data` wrapper: -```json { "op": "create", "object": "tasks", "args": { "name": "Review PR", - "priority": "high" + "priority": "high", + "assignee_id": "user_123", + "due_date": "2024-01-20" } } ``` @@ -1215,7 +1201,7 @@ for (const order of orders) { - **[Query Language Specification](../spec/query-language.md)** - Deep dive into filter syntax - **[Actions Guide](../guide/logic-actions.md)** - Building custom RPC operations - **[Server Integration](../guide/server-integration.md)** - Deploying ObjectQL APIs -- **[Authentication Guide](../guide/authentication.md)** *(Coming Soon)* +- **[Authentication Guide](./authentication.md)** - Securing your APIs - **[GraphQL API](./graphql.md)** *(Planned)* --- diff --git a/docs/api/authentication.md b/docs/api/authentication.md index f7712e70..daec3656 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -566,7 +566,8 @@ const authenticate = async (req, res, next) => { const decoded = jwt.verify(token, process.env.JWT_SECRET); // Fetch full user context from database if needed - const userContext = await app.getContext().object('users').findOne(decoded.userId); + const ctx = app.createContext({ userId: decoded.userId }); + const userContext = await ctx.object('users').findOne(decoded.userId); req.user = { id: userContext.id, From deb5ebfcd094a76c866304e8f4f452c974173978 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:28:51 +0000 Subject: [PATCH 5/8] Improve API documentation based on second code review Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- docs/api/authentication.md | 27 ++++++++++++++++++++++++--- docs/api/quick-reference.md | 8 +++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/api/authentication.md b/docs/api/authentication.md index daec3656..fb2f7867 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -32,12 +32,14 @@ const server = express(); // Middleware to verify JWT and attach user context server.use('/api/objectql', async (req, res, next) => { - const token = req.headers.authorization?.replace('Bearer ', ''); + const authHeader = req.headers.authorization; - if (!token) { + if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: { code: 'UNAUTHORIZED', message: 'No token provided' }}); } + const token = authHeader.substring(7); // Remove 'Bearer ' prefix + try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; // Attach user to request @@ -594,7 +596,10 @@ server.all('/api/objectql', createNodeHandler(app)); server.post('/api/auth/login', async (req, res) => { const { email, password } = req.body; - // Verify credentials (example - use proper password hashing!) + // Verify credentials + // Use bcrypt or argon2 for password hashing: + // const bcrypt = require('bcryptjs'); + // const match = await bcrypt.compare(password, user.password_hash); const user = await verifyCredentials(email, password); if (!user) { @@ -613,6 +618,22 @@ server.post('/api/auth/login', async (req, res) => { res.json({ token, user }); }); +// Example verifyCredentials function using bcrypt +async function verifyCredentials(email: string, password: string) { + const bcrypt = require('bcryptjs'); + + // Find user by email + const ctx = app.createContext({}); + const user = await ctx.object('users').findOne({ filters: [['email', '=', email]] }); + + if (!user) return null; + + // Compare password with hash + const match = await bcrypt.compare(password, user.password_hash); + + return match ? user : null; +} + server.listen(3000); ``` diff --git a/docs/api/quick-reference.md b/docs/api/quick-reference.md index a94d72b8..f2e4681d 100644 --- a/docs/api/quick-reference.md +++ b/docs/api/quick-reference.md @@ -73,11 +73,9 @@ or with filters: "op": "create", "object": "tasks", "args": { - "data": { - "name": "Complete documentation", - "priority": "high", - "due_date": "2024-01-20" - } + "name": "Complete documentation", + "priority": "high", + "due_date": "2024-01-20" } } ``` From ed3da8136896de0c2a0cac6c8661b3983b5e5896 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 11 Jan 2026 08:30:43 +0000 Subject: [PATCH 6/8] Fix verifyCredentials scope issue in auth example Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com> --- docs/api/authentication.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api/authentication.md b/docs/api/authentication.md index fb2f7867..f6b9fb0e 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -622,7 +622,7 @@ server.post('/api/auth/login', async (req, res) => { async function verifyCredentials(email: string, password: string) { const bcrypt = require('bcryptjs'); - // Find user by email + // Find user by email (using the app instance from outer scope) const ctx = app.createContext({}); const user = await ctx.object('users').findOne({ filters: [['email', '=', email]] }); @@ -634,6 +634,8 @@ async function verifyCredentials(email: string, password: string) { return match ? user : null; } +server.listen(3000); + server.listen(3000); ``` From a1f41cc589686039bf4b268ef14244d8adf3ff0e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:49:03 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20index.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/api/index.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/api/index.md b/docs/api/index.md index d70570de..0c04fdcd 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -85,11 +85,9 @@ curl -X POST http://localhost:3000/api/objectql \ "op": "create", "object": "tasks", "args": { - "data": { - "name": "Complete documentation", - "priority": "high", - "due_date": "2024-01-20" - } + "name": "Complete documentation", + "priority": "high", + "due_date": "2024-01-20" } }' ``` From cc84dddb45017d1133b43244e8a78a3c4336be92 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:49:41 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20authentication.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/api/authentication.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/api/authentication.md b/docs/api/authentication.md index f6b9fb0e..b2890a63 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -634,8 +634,6 @@ async function verifyCredentials(email: string, password: string) { return match ? user : null; } -server.listen(3000); - server.listen(3000); ```