diff --git a/.gitignore b/.gitignore index fe08c4ea..49dad5ae 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,11 @@ dist docs/.vitepress/cache docs/.vitepress/dist +# Fumadocs / Next.js +apps/site/.next +apps/site/out +apps/site/.source + # Database *.sqlite3 *.db diff --git a/apps/site/.eslintrc.json b/apps/site/.eslintrc.json new file mode 100644 index 00000000..bffb357a --- /dev/null +++ b/apps/site/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/apps/site/.gitignore b/apps/site/.gitignore new file mode 100644 index 00000000..1a970004 --- /dev/null +++ b/apps/site/.gitignore @@ -0,0 +1,36 @@ +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +# fumadocs +.source diff --git a/apps/site/app/docs/[[...slug]]/page.tsx b/apps/site/app/docs/[[...slug]]/page.tsx new file mode 100644 index 00000000..04aabcaa --- /dev/null +++ b/apps/site/app/docs/[[...slug]]/page.tsx @@ -0,0 +1,40 @@ +import { source } from '@/lib/source'; +import type { Metadata } from 'next'; +import { DocsPage, DocsBody, DocsDescription, DocsTitle } from 'fumadocs-ui/page'; +import { notFound } from 'next/navigation'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; + +export default async function Page({ + params, +}: { + params: { slug?: string[] }; +}) { + const page = source.getPage(params.slug); + if (!page) notFound(); + + const MDX = page.data.body; + + return ( + + {page.data.title} + {page.data.description} + + + + + ); +} + +export async function generateStaticParams() { + return source.generateParams(); +} + +export function generateMetadata({ params }: { params: { slug?: string[] } }): Metadata { + const page = source.getPage(params.slug); + if (!page) notFound(); + + return { + title: page.data.title, + description: page.data.description, + }; +} diff --git a/apps/site/app/docs/layout.tsx b/apps/site/app/docs/layout.tsx new file mode 100644 index 00000000..fd6cb998 --- /dev/null +++ b/apps/site/app/docs/layout.tsx @@ -0,0 +1,12 @@ +import { source } from '@/lib/source'; +import type { ReactNode } from 'react'; +import { DocsLayout } from 'fumadocs-ui/layout'; +import { baseOptions } from '@/app/layout.config'; + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/apps/site/app/globals.css b/apps/site/app/globals.css new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/apps/site/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/site/app/layout.config.tsx b/apps/site/app/layout.config.tsx new file mode 100644 index 00000000..93f5c0bc --- /dev/null +++ b/apps/site/app/layout.config.tsx @@ -0,0 +1,17 @@ +import type { DocsLayoutProps } from 'fumadocs-ui/layout'; +// @ts-ignore: optional dev dependency for icons (some environments may not have types) +import { Book, Code2, FileText, Sparkles } from 'lucide-react'; + +export const baseOptions: Omit = { + nav: { + title: 'ObjectQL', + }, + links: [ + { + text: 'Documentation', + url: '/docs', + active: 'nested-url', + }, + ], + githubUrl: 'https://github.com/objectstack-ai/objectql', +}; diff --git a/apps/site/app/layout.tsx b/apps/site/app/layout.tsx new file mode 100644 index 00000000..353c26bb --- /dev/null +++ b/apps/site/app/layout.tsx @@ -0,0 +1,13 @@ +import './globals.css'; +import type { ReactNode } from 'react'; +import { RootProvider } from 'fumadocs-ui/provider'; + +export default function Layout({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/apps/site/app/page.tsx b/apps/site/app/page.tsx new file mode 100644 index 00000000..e89b14e5 --- /dev/null +++ b/apps/site/app/page.tsx @@ -0,0 +1,10 @@ +export default function HomePage() { + return ( +
+

ObjectQL Documentation

+

+ Visit /docs to view the documentation +

+
+ ); +} diff --git a/apps/site/content/docs/IMPLEMENTATION_SUMMARY.mdx b/apps/site/content/docs/IMPLEMENTATION_SUMMARY.mdx new file mode 100644 index 00000000..604dcb83 --- /dev/null +++ b/apps/site/content/docs/IMPLEMENTATION_SUMMARY.mdx @@ -0,0 +1,356 @@ +--- +title: ObjectQL Attachment API - Implementation Summary +--- + +# ObjectQL Attachment API - Implementation Summary + +## Overview + +This implementation adds comprehensive file attachment functionality to ObjectQL, enabling seamless file upload, storage, and download capabilities through REST API endpoints. + +## What Has Been Implemented + +### 1. Core Infrastructure + +#### File Storage Abstraction (`IFileStorage`) +- **Interface**: Defines contract for storage providers +- **LocalFileStorage**: Production-ready local filesystem storage +- **MemoryFileStorage**: Lightweight in-memory storage for testing +- **Extensible**: Easy to add S3, Azure Blob, Google Cloud Storage + +```typescript +interface IFileStorage { + save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise; + get(fileId: string): Promise; + delete(fileId: string): Promise; + getPublicUrl(fileId: string): string; +} +``` + +#### Type Definitions +- `AttachmentData`: File metadata structure +- `ImageAttachmentData`: Extended metadata for images +- `FileStorageOptions`: Storage configuration +- Full TypeScript type safety throughout + +### 2. HTTP API Endpoints + +All endpoints are automatically available when using `createNodeHandler`: + +| Endpoint | Method | Purpose | +|----------|--------|---------| +| `/api/files/upload` | POST | Upload single file | +| `/api/files/upload/batch` | POST | Upload multiple files | +| `/api/files/:fileId` | GET | Download file | + +### 3. File Validation + +Automatic validation based on ObjectQL field definitions: + +```yaml +# Object definition +receipt: + type: file + accept: ['.pdf', '.jpg', '.png'] + max_size: 5242880 # 5MB + min_size: 1024 # 1KB +``` + +Validation includes: +- File type/extension checking +- File size limits (min/max) +- Detailed error messages with error codes + +### 4. Multipart Form Data Parser + +Native implementation without external dependencies: +- Parses `multipart/form-data` requests +- Handles file uploads and form fields +- Support for multiple files +- Binary-safe file handling + +### 5. Testing + +**Test Coverage**: 15+ tests across multiple suites +- Storage operations (save, get, delete) +- File validation (size, type, extensions) +- Integration examples +- **All 77 tests passing** in the package + +## Usage + +### Server Setup + +```typescript +import { ObjectQL } from '@objectql/core'; +import { createNodeHandler, LocalFileStorage } from '@objectql/server'; + +const app = new ObjectQL({ /* ... */ }); + +// Define object with file field +app.registerObject({ + name: 'expense', + fields: { + receipt: { + type: 'file', + accept: ['.pdf', '.jpg', '.png'], + max_size: 5242880 + } + } +}); + +await app.init(); + +// Configure storage +const storage = new LocalFileStorage({ + baseDir: './uploads', + baseUrl: 'http://localhost:3000/api/files' +}); + +// Create server with file support +const handler = createNodeHandler(app, { fileStorage: storage }); +const server = http.createServer(handler); +server.listen(3000); +``` + +### Client Upload + +```bash +# Upload file +curl -X POST http://localhost:3000/api/files/upload \ + -F "file=@receipt.pdf" \ + -F "object=expense" \ + -F "field=receipt" + +# Create record with file +curl -X POST http://localhost:3000/api/objectql \ + -H "Content-Type: application/json" \ + -d '{ + "op": "create", + "object": "expense", + "args": { + "expense_number": "EXP-001", + "receipt": { + "id": "abc123", + "name": "receipt.pdf", + "url": "http://localhost:3000/api/files/uploads/expense/abc123.pdf", + "size": 245760, + "type": "application/pdf" + } + } + }' +``` + +### JavaScript/TypeScript + +```typescript +// Upload file +const formData = new FormData(); +formData.append('file', file); +formData.append('object', 'expense'); +formData.append('field', 'receipt'); + +const uploadRes = await fetch('/api/files/upload', { + method: 'POST', + body: formData +}); + +const { data: uploadedFile } = await uploadRes.json(); + +// Create expense with file +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + expense_number: 'EXP-001', + amount: 125.50, + receipt: uploadedFile + } + }) +}); +``` + +## Documentation + +### English Documentation +- **API Specification**: `docs/api/attachments.md` + - Updated with server implementation section + - Storage configuration examples + - Custom storage implementation guide + - Environment variables reference + +- **Usage Examples**: `docs/examples/file-upload-example.md` + - Complete server setup code + - cURL examples + - JavaScript/TypeScript client code + - React component examples + +### Chinese Documentation +- **Implementation Guide**: `docs/examples/README_CN.md` + - Architecture overview in Chinese + - Detailed implementation explanation + - Usage examples with Chinese comments + - Extension guide for custom storage + +## Files Modified/Created + +### Core Implementation +- `packages/runtime/server/src/types.ts` - Type definitions +- `packages/runtime/server/src/storage.ts` - Storage implementations +- `packages/runtime/server/src/file-handler.ts` - Upload/download handlers +- `packages/runtime/server/src/adapters/node.ts` - HTTP endpoint routing +- `packages/runtime/server/src/index.ts` - Module exports + +### Testing +- `packages/runtime/server/test/storage.test.ts` - Storage tests +- `packages/runtime/server/test/file-validation.test.ts` - Validation tests +- `packages/runtime/server/test/file-upload-integration.example.ts` - Integration example + +### Documentation +- `docs/api/attachments.md` - Updated API specification +- `docs/examples/file-upload-example.md` - Usage examples +- `docs/examples/README_CN.md` - Chinese implementation guide + +### Examples +- `examples/demo-file-upload.ts` - Working demo script + +## Architecture Decisions + +### 1. Storage Abstraction +**Why**: Allows flexibility to switch between local filesystem, S3, Azure Blob, etc. without changing business logic. + +### 2. Native Multipart Parser +**Why**: Eliminates dependency on external libraries like `multer` or `formidable`, keeping the package lightweight and reducing security surface. + +### 3. Validation in Field Config +**Why**: Centralized validation rules in object definitions, ensuring consistency between frontend and backend. + +### 4. Async File Operations +**Why**: Uses `fs.promises` API to avoid blocking the event loop, improving server performance. + +### 5. Memory Storage for Testing +**Why**: Enables fast, dependency-free testing without disk I/O. + +## Environment Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `OBJECTQL_UPLOAD_DIR` | `./uploads` | Directory for local file storage | +| `OBJECTQL_BASE_URL` | `http://localhost:3000/api/files` | Base URL for file access | + +## Security Considerations + +1. **File Type Validation**: Enforced through `accept` field config +2. **File Size Limits**: Enforced through `max_size`/`min_size` config +3. **Authentication Placeholder**: Current implementation includes placeholder for JWT/token validation +4. **Path Traversal Protection**: File IDs are generated, not user-controlled +5. **MIME Type Verification**: Stored alongside file metadata + +## Performance Characteristics + +- **Async I/O**: All file operations use async APIs +- **Streaming**: Files are handled as buffers for efficiency +- **Memory Storage**: O(1) lookup for test scenarios +- **Local Storage**: Organized folder structure for faster file system operations + +## Future Enhancements + +The following features are planned but not yet implemented: + +1. **Image Processing** + - Thumbnail generation (`/api/files/:fileId/thumbnail`) + - Image resizing (`/api/files/:fileId/preview?width=300&height=300`) + - Format conversion + +2. **Cloud Storage** (✅ Implementation guide available) + - ✅ **AWS S3 adapter** - [Full implementation guide](./examples/s3-integration-guide-cn.md) and [production code](./examples/s3-storage-implementation.ts) + - Azure Blob Storage adapter + - Google Cloud Storage adapter + - Alibaba OSS adapter + +3. **Advanced Features** + - ✅ **Signed URLs** - Implemented in S3 adapter example + - File access permissions/ACL + - Virus scanning integration + - ✅ **CDN integration** - CloudFront support in S3 adapter + - Automatic image optimization + +4. **Monitoring** + - Upload progress tracking + - Storage quota management + - Usage analytics + +## Testing Instructions + +```bash +# Run all server tests +cd packages/runtime/server +pnpm test + +# Run specific test suites +pnpm test storage.test.ts +pnpm test file-validation.test.ts + +# Build the package +pnpm run build + +# Run the demo +cd ../../.. +ts-node examples/demo-file-upload.ts +``` + +## Migration Guide + +For existing ObjectQL projects: + +1. **Update Dependencies** + ```bash + pnpm update @objectql/server + ``` + +2. **Configure Storage** + ```typescript + import { LocalFileStorage } from '@objectql/server'; + + const storage = new LocalFileStorage({ + baseDir: process.env.OBJECTQL_UPLOAD_DIR || './uploads', + baseUrl: process.env.OBJECTQL_BASE_URL || 'http://localhost:3000/api/files' + }); + ``` + +3. **Update Server Initialization** + ```typescript + const handler = createNodeHandler(app, { fileStorage: storage }); + ``` + +4. **Add File Fields to Objects** + ```yaml + receipt: + type: file + accept: ['.pdf', '.jpg', '.png'] + max_size: 5242880 + ``` + +No breaking changes to existing APIs or functionality. + +## Conclusion + +This implementation provides a production-ready, extensible file attachment system for ObjectQL that: +- ✅ Follows ObjectQL architectural principles +- ✅ Maintains zero-dependency core approach +- ✅ Provides comprehensive documentation +- ✅ Includes thorough test coverage +- ✅ Supports multiple storage backends +- ✅ Offers excellent developer experience + +The implementation is ready for use in production applications while maintaining flexibility for future enhancements. + +--- + +**Implementation Date**: January 2026 +**ObjectQL Version**: 1.8.0+ +**Author**: GitHub Copilot +**Status**: ✅ Complete and Tested diff --git a/apps/site/content/docs/METADATA_TYPES_IMPLEMENTATION.mdx b/apps/site/content/docs/METADATA_TYPES_IMPLEMENTATION.mdx new file mode 100644 index 00000000..f9d2faf8 --- /dev/null +++ b/apps/site/content/docs/METADATA_TYPES_IMPLEMENTATION.mdx @@ -0,0 +1,461 @@ +--- +title: Metadata Type Definitions - Implementation Summary +--- + +# Metadata Type Definitions - Implementation Summary + +## Overview + +This PR implements complete TypeScript type definitions for all ObjectQL metadata formats. Previously, only some metadata types (Object, Validation, Permission, etc.) had TypeScript definitions. This implementation adds the missing types to achieve 100% coverage. + +## New Type Definitions Added + +### 1. ViewConfig (`packages/foundation/types/src/view.ts`) + +**Purpose**: Define list views, grid views, and other data visualization views. + +**File Pattern**: `*.view.yml` + +**Example Usage**: +```yaml +# projects.view.yml +name: all_projects +label: "Active Projects" +type: list +object: projects +columns: + - field: name + width: 250 + - field: status + width: 120 +filters: + - field: status + operator: "!=" + value: "archived" +``` + +**Key Types**: +- `ViewConfig` - Main configuration interface +- `ViewType` - View types (list, kanban, calendar, timeline, gallery, map, pivot) +- `ViewColumn` - Column display configuration +- `ViewFilter` - Filter conditions +- `ViewSort` - Sorting configuration +- View-specific configs: `KanbanViewConfig`, `CalendarViewConfig`, `TimelineViewConfig`, `GalleryViewConfig`, `MapViewConfig` + +**Features**: +- Column formatting and display options +- Complex filter conditions with logical grouping +- Grouping and pagination +- Export capabilities +- Search configuration +- Inline editing support + +--- + +### 2. WorkflowConfig (`packages/foundation/types/src/workflow.ts`) + +**Purpose**: Define workflows, approvals, and automation processes. + +**File Pattern**: `*.workflow.yml` + +**Example Usage**: +```yaml +# project_approval.workflow.yml +name: project_approval +label: "High Budget Project Approval" +type: approval +object: projects +trigger: + event: create_or_update + conditions: + - field: budget + operator: ">" + value: 50000 +steps: + - name: manager_review + label: "Manager Review" + type: approval + assignee: + type: role + role: manager + actions: + approve: + label: "Approve" + next_step: finance_review +``` + +**Key Types**: +- `WorkflowConfig` - Main configuration interface +- `WorkflowType` - Workflow types (approval, automation, scheduled, sequential, parallel) +- `WorkflowTrigger` - Trigger event configuration +- `WorkflowStep` - Individual step definition +- `WorkflowStepType` - Step types (approval, action, notification, field_update, etc.) +- `WorkflowAssignee` - Assignee configuration for approvals +- `WorkflowInstance` - Runtime execution tracking + +**Features**: +- Multi-step approval chains +- Conditional branching +- Field updates and record creation +- Notifications (email, SMS, push, in-app) +- Wait conditions and loops +- Webhook integrations +- Error handling and retry logic +- Parallel execution support + +--- + +### 3. ReportConfig (`packages/foundation/types/src/report.ts`) + +**Purpose**: Define reports, data summaries, and analytics. + +**File Pattern**: `*.report.yml` + +**Example Usage**: +```yaml +# project_status.report.yml +name: project_status_summary +label: "Project Status Report" +type: summary +object: projects +columns: + - field: name + label: Project Name + - field: budget + label: Budget + format: currency +groupings: + - field: status + label: Status +aggregations: + - field: budget + function: sum + label: Total Budget + - field: _id + function: count + label: Project Count +``` + +**Key Types**: +- `ReportConfig` - Main configuration interface +- `ReportType` - Report types (tabular, summary, matrix, chart, dashboard) +- `ChartType` - Chart types (bar, line, pie, scatter, funnel, gauge, etc.) +- `ReportGrouping` - Grouping configuration +- `ReportAggregation` - Aggregation/calculation configuration +- `AggregationFunction` - Aggregation functions (count, sum, avg, min, max, etc.) +- `ReportChartConfig` - Chart visualization configuration +- `ReportExportConfig` - Export settings (PDF, XLSX, CSV) +- `ReportScheduleConfig` - Automated report scheduling + +**Features**: +- Multiple grouping levels +- Complex aggregations and formulas +- Chart visualizations +- Matrix/pivot reports +- Export to multiple formats +- Scheduled report generation +- Drill-down capabilities +- Caching support + +--- + +### 4. FormConfig (`packages/foundation/types/src/form.ts`) + +**Purpose**: Define forms, layouts, and field arrangements. + +**File Pattern**: `*.form.yml` + +**Example Usage**: +```yaml +# project_form.form.yml +name: project_main_form +label: "Project Details" +type: edit +object: projects +layout: + tabs: + - label: "General Info" + sections: + - label: "Basic Details" + columns: 2 + fields: + - name + - owner + - status + - priority + - label: "Planning" + sections: + - label: "Schedule & Budget" + columns: 2 + fields: + - start_date + - end_date + - budget +``` + +**Key Types**: +- `FormConfig` - Main configuration interface +- `FormType` - Form types (create, edit, view, wizard, quick_create) +- `FormLayoutType` - Layout types (single/two/three column, tabs, accordion) +- `FormField` - Field display configuration +- `FormSection` - Section grouping +- `FormTab` - Tab configuration +- `WizardStep` - Wizard step for multi-step forms +- `FormAction` - Form buttons and actions +- `FormValidationConfig` - Validation behavior +- `FormAutosaveConfig` - Autosave configuration +- `FormState` - Runtime form state + +**Features**: +- Multiple layout types +- Tab and section organization +- Multi-step wizard forms +- Conditional visibility +- Field-level overrides +- Custom validation +- Autosave functionality +- Header and footer customization + +--- + +## Type System Architecture + +### Zero Dependencies Principle + +All new types follow the "Constitution" rule: +- Located in `@objectql/types` package +- **Zero dependencies** on other packages +- Pure TypeScript interfaces, types, and enums +- Can be used by both Backend (`@objectql/core`) and Frontend (`@object-ui/*`) + +### Consistency with Existing Types + +The new types follow the same patterns as existing metadata types: + +1. **Config Interface Pattern**: Main interface named `*Config` + - `ObjectConfig`, `ValidationConfig`, `PermissionConfig` (existing) + - `ViewConfig`, `WorkflowConfig`, `ReportConfig`, `FormConfig` (new) + +2. **Comprehensive JSDoc**: All interfaces and properties documented + +3. **Nested Type Definitions**: Complex configurations broken into sub-interfaces + +4. **Enum-like Types**: Use TypeScript union types for enums (e.g., `type ViewType = 'list' | 'kanban' | ...`) + +5. **Optional AI Context**: All main configs include optional `ai_context` for AI-assisted generation + +### Integration Points + +```typescript +// Object definition references validation +interface ObjectConfig { + validation?: { + rules?: AnyValidationRule[]; + }; +} + +// View references filters (similar to validation conditions) +interface ViewFilter { + field: string; + operator: ValidationOperator; + value?: any; +} + +// Workflow references validation conditions +interface WorkflowTrigger { + conditions?: ValidationCondition[]; +} + +// Form uses field config for overrides +interface FormField { + name: string; + // ... can override FieldConfig properties +} +``` + +## File Structure + +``` +packages/foundation/types/src/ +├── index.ts # Exports all types (updated) +├── view.ts # NEW: View type definitions +├── workflow.ts # NEW: Workflow type definitions +├── report.ts # NEW: Report type definitions +├── form.ts # NEW: Form type definitions +├── object.ts # Existing +├── validation.ts # Existing +├── permission.ts # Existing +├── field.ts # Existing +└── ... (other existing files) +``` + +## Metadata Coverage + +| Metadata Type | File Pattern | TypeScript Type | Status | +|--------------|--------------|-----------------|--------| +| Object | `*.object.yml` | `ObjectConfig` | ✅ Existing | +| Validation | `*.validation.yml` | `ValidationConfig` | ✅ Existing | +| Permission | `*.permission.yml` | `PermissionConfig` | ✅ Existing | +| Hook | `*.hook.ts` | `ObjectHookDefinition` | ✅ Existing | +| Action | `*.action.ts` | `ActionConfig` | ✅ Existing | +| Page | `*.page.yml` | `PageConfig` | ✅ Existing | +| Menu | `*.menu.yml` | `MenuConfig` | ✅ Existing | +| App | `*.app.yml` | `AppConfig` | ✅ Existing | +| Migration | `*.migration.yml` | `MigrationConfig` | ✅ Existing | +| **View** | `*.view.yml` | `ViewConfig` | ✅ **NEW** | +| **Workflow** | `*.workflow.yml` | `WorkflowConfig` | ✅ **NEW** | +| **Report** | `*.report.yml` | `ReportConfig` | ✅ **NEW** | +| **Form** | `*.form.yml` | `FormConfig` | ✅ **NEW** | + +**Result: 100% metadata type coverage achieved! 🎉** + +## Build Verification + +```bash +cd packages/foundation/types +npm run build +# ✓ TypeScript compilation successful +# ✓ All .d.ts files generated +# ✓ Types exported from index.ts +``` + +Compiled outputs: +- `dist/view.d.ts` (8.5KB) + `dist/view.js` +- `dist/workflow.d.ts` (11KB) + `dist/workflow.js` +- `dist/report.d.ts` (7.9KB) + `dist/report.js` +- `dist/form.d.ts` (8.7KB) + `dist/form.js` + +## Usage Examples + +### Using View Types in TypeScript + +```typescript +import { ViewConfig, ViewType } from '@objectql/types'; + +const userListView: ViewConfig = { + name: 'active_users', + label: 'Active Users', + object: 'users', + type: 'list', + columns: [ + { field: 'name', width: 200, sortable: true }, + { field: 'email', width: 250, sortable: true }, + { field: 'status', width: 120, badge: true } + ], + filters: [ + { field: 'status', operator: '=', value: 'active' } + ], + enable_search: true, + enable_export: true +}; +``` + +### Using Workflow Types in TypeScript + +```typescript +import { WorkflowConfig, WorkflowType } from '@objectql/types'; + +const approvalWorkflow: WorkflowConfig = { + name: 'expense_approval', + label: 'Expense Approval', + type: 'approval', + object: 'expenses', + trigger: { + event: 'create', + conditions: [ + { field: 'amount', operator: '>', value: 1000 } + ] + }, + steps: [ + { + name: 'manager_approval', + type: 'approval', + assignee: { type: 'field', field: 'manager_id' }, + actions: { + approve: { label: 'Approve', outcome: 'approved' }, + reject: { label: 'Reject', outcome: 'rejected' } + } + } + ] +}; +``` + +### Using Report Types in TypeScript + +```typescript +import { ReportConfig, ChartType } from '@objectql/types'; + +const salesReport: ReportConfig = { + name: 'monthly_sales', + label: 'Monthly Sales Report', + type: 'summary', + object: 'orders', + groupings: [ + { field: 'status', label: 'Order Status' } + ], + aggregations: [ + { field: 'total', function: 'sum', label: 'Total Revenue' }, + { field: '_id', function: 'count', label: 'Order Count' } + ], + chart: { + type: 'bar', + x_axis: 'status', + y_axis: 'total', + show_legend: true + } +}; +``` + +### Using Form Types in TypeScript + +```typescript +import { FormConfig, FormType } from '@objectql/types'; + +const userForm: FormConfig = { + name: 'user_create_form', + label: 'Create User', + type: 'create', + object: 'users', + layout: 'two_column', + sections: [ + { + label: 'User Information', + columns: 2, + fields: [ + { name: 'name', required: true }, + { name: 'email', required: true }, + { name: 'phone' }, + { name: 'role', required: true } + ] + } + ], + actions: [ + { name: 'save', label: 'Save', type: 'submit', variant: 'primary' }, + { name: 'cancel', label: 'Cancel', type: 'cancel', variant: 'secondary' } + ] +}; +``` + +## Benefits + +1. **Type Safety**: Full TypeScript support for all metadata formats +2. **IDE Support**: IntelliSense, auto-completion, and inline documentation +3. **Validation**: Catch errors at compile-time instead of runtime +4. **Consistency**: Unified type system across the entire ObjectQL ecosystem +5. **Documentation**: Types serve as living documentation +6. **AI-Friendly**: Comprehensive types enable better AI code generation + +## Future Enhancements + +The type system is designed to be extensible. Potential additions: + +- Dashboard layout types (composition of multiple pages/widgets) +- Integration types (external API configurations) +- Theme/styling types (UI customization metadata) +- Deployment types (environment configurations) + +--- + +**Implementation Date**: January 14-15, 2026 +**Author**: ObjectQL Lead Architect +**Package**: `@objectql/types@1.8.4` diff --git a/apps/site/content/docs/ai/building-apps.mdx b/apps/site/content/docs/ai/building-apps.mdx new file mode 100644 index 00000000..4b537a04 --- /dev/null +++ b/apps/site/content/docs/ai/building-apps.mdx @@ -0,0 +1,88 @@ +--- +title: Building AI-Native Apps +--- + +# Building AI-Native Apps + +ObjectQL is engineered to be the ideal data layer for AI Agents and LLMs. By providing a **Structure-First** protocol (JSON AST) instead of raw strings (SQL), it drastically reduces hallucinations and injection risks. + +## 1. Why ObjectQL for AI? + +| Feature | SQL / Traditional ORM | ObjectQL | +| :--- | :--- | :--- | +| **Output** | Unstructured String | **Strict JSON** | +| **Safety** | Injection Vulnerable | **Injection Safe** | +| **Context** | Heavy DDL dumps | **Lightweight Scoped Schema** | + +LLMs excel at generating JSON. ObjectQL lets the LLM speak its native language. + +## 2. Semantic Search (RAG) + +ObjectQL has first-class support for Vector Search. You don't need a separate vector database (like Pinecone) or generic ORM hacks. + +### Configuration + +Enable search in your `*.object.yml`. + +```yaml +# knowledge.object.yml +name: knowledge +fields: + title: { type: text } + content: { type: textarea } + +# Enable AI capabilities +ai: + search: + enabled: true + fields: [title, content] # Fields to embed + model: text-embedding-3-small +``` + +### Usage + +When enabled, the driver manages the embeddings automatically. You can then search using natural language. + +```typescript +// Search for "How to reset password" +const results = await objectql.search('knowledge', 'How to reset password'); + +// returns: [{ id: 1, title: 'Reset Config', _score: 0.89 }, ...] +``` + +## 3. Explicit Vector Columns + +For advanced use cases (e.g., Image Search or Multi-modal embeddings), you can define raw vector columns. + +```yaml +fields: + image_url: + type: url + + clip_embedding: + type: vector + dimension: 512 + index: true # Create IVFFlat/HNSW index +``` + +## 4. LLM to Query (Text-to-SQL alternative) + +Instead of asking an LLM to write SQL, ask it to write ObjectQL JSON. + +**Prompt Pattern:** + +```text +You are a data assistant. +Schema: +- Object: Task (fields: title, status, priority) + +User: "Find my high priority tasks" + +Output JSON in ObjectQL format: +{ + "entity": "task", + "filters": [["priority", "=", "High"]] +} +``` + +This output can be safely executed by the ObjectQL engine without fear of `DROP TABLE` injections. diff --git a/apps/site/content/docs/ai/cli-usage.mdx b/apps/site/content/docs/ai/cli-usage.mdx new file mode 100644 index 00000000..f6fcf410 --- /dev/null +++ b/apps/site/content/docs/ai/cli-usage.mdx @@ -0,0 +1,316 @@ +--- +title: AI-Powered CLI +--- + +# AI-Powered CLI + +The ObjectQL CLI provides AI-powered commands to generate and validate applications using natural language. + +## Overview + +The `objectql ai` command provides interactive and automated application generation with built-in validation and testing. + +## Prerequisites + +Set your OpenAI API key as an environment variable: + +```bash +export OPENAI_API_KEY=sk-your-api-key-here +``` + +## Commands + +### Interactive Mode (Default) + +The easiest way to build applications - just type: + +```bash +objectql ai +``` + +This starts an interactive conversational session where you can: +- Describe what you want to build in natural language +- Request changes and improvements iteratively +- Get suggestions for next steps +- See files generated in real-time + +**Example Session:** + +``` +$ objectql ai +💬 ObjectQL AI Assistant + +What would you like to build today? +> A blog system with posts, comments, and categories + +Great! I'll create a blog system for you... +✓ Generated: post.object.yml +✓ Generated: comment.object.yml +✓ Generated: category.object.yml +✓ Generated: post.hook.ts +✓ Generated: post.test.ts + +What would you like to add or modify? +> Add tags to posts + +Adding tag support... +✓ Generated: tag.object.yml +✓ Updated: post.object.yml + +Type "done" to finish, or continue refining your app. +> done + +📁 Application saved to ./src +``` + +**Specify Output Directory:** + +```bash +objectql ai ./my-app +``` + +### One-Shot Generation + +Generate a complete application from a single description without interaction: + +```bash +objectql ai generate -d "A CRM system with customers, contacts, and opportunities" -o ./src +``` + +**Options:** +- `-d, --description ` - Application description (required) +- `-o, --output ` - Output directory (default: `./src`) +- `-t, --type ` - Generation type: `basic`, `complete`, or `custom` (default: `custom`) + +**Generation Types:** + +- `basic` - Minimal metadata (objects only) +- `complete` - Full metadata (objects, forms, views, actions, hooks, tests) +- `custom` - AI decides based on description (recommended) + +**Examples:** + +```bash +# Generate complete CRM +objectql ai generate \ + -d "Customer relationship management with sales pipeline" \ + -t complete \ + -o ./crm + +# Generate simple inventory tracker +objectql ai generate \ + -d "Track products with quantities and locations" \ + -t basic + +# Let AI decide what's needed +objectql ai generate \ + -d "E-commerce platform with products, orders, and payments" +``` + +**What Gets Generated:** + +For a `complete` application, you get: + +1. **Metadata Files (YAML)** + - `*.object.yml` - Data entities + - `*.validation.yml` - Validation rules + - `*.permission.yml` - Access control + - `*.workflow.yml` - Automation + - `*.data.yml` - Seed data + +2. **TypeScript Implementation Files** + - `*.action.ts` - Custom business operations + - `*.hook.ts` - Lifecycle triggers (beforeCreate, afterUpdate, etc.) + +3. **Test Files** + - `*.test.ts` - Jest tests for business logic + +### Validation + +Validate existing metadata files with AI-powered analysis: + +```bash +objectql ai validate ./src +``` + +**Options:** +- `` - Path to metadata directory (required) +- `--fix` - Automatically fix issues where possible +- `-v, --verbose` - Show detailed validation output + +**What Gets Checked:** +- ✅ YAML syntax +- ✅ ObjectQL specification compliance +- ✅ Business logic consistency +- ✅ Data model best practices +- ✅ Security considerations +- ✅ Performance implications +- ✅ Field type correctness +- ✅ Relationship integrity + +**Example:** + +```bash +$ objectql ai validate ./src -v + +🔍 Validating metadata files... + +✓ user.object.yml - Valid +⚠ order.object.yml - 2 warnings + - Line 15: Consider adding index on 'customer_id' field for query performance + - Line 23: 'total' field should use 'currency' type instead of 'number' + +❌ product.object.yml - 1 error + - Line 10: Invalid field type 'string', use 'text' instead + +📊 Summary: + Files checked: 3 + Errors: 1 + Warnings: 2 + Info: 0 +``` + +### Chat Assistant + +Get help and guidance about ObjectQL concepts: + +```bash +objectql ai chat +``` + +**With Initial Prompt:** + +```bash +objectql ai chat -p "How do I create a lookup relationship?" +``` + +**Example Session:** + +``` +$ objectql ai chat +🤖 ObjectQL AI Assistant + +Ask me anything about ObjectQL! +> How do I add email validation to a field? + +You can add email validation in several ways: + +1. Use the built-in 'email' field type: + fields: + email: + type: email + required: true + +2. Or add validation rules: + fields: + contact_email: + type: text + validation: + format: email + +> What about custom validation logic? + +For custom validation, use a validation hook... +``` + +## Complete Workflow Example + +Here's a complete workflow from generation to deployment: + +```bash +# 1. Set API key +export OPENAI_API_KEY=sk-your-key + +# 2. Generate application (interactive) +objectql ai +> A project management system with tasks, projects, and teams +> done + +# 3. Validate generated files +objectql ai validate ./src -v + +# 4. Fix any issues +objectql ai validate ./src --fix + +# 5. Test the application +objectql serve + +# 6. Get help if needed +objectql ai chat -p "How do I add user authentication?" +``` + +## Tips & Best Practices + +### Writing Good Descriptions + +**Good:** +```bash +objectql ai generate -d "Inventory management with products, warehouses, stock movements, and reorder points. Include barcode scanning support and low stock alerts." +``` + +**Not as good:** +```bash +objectql ai generate -d "inventory app" +``` + +**Tips:** +- Be specific about entities and relationships +- Mention key features and business rules +- Include any special requirements (e.g., "with approval workflow") +- Specify important fields or attributes + +### Interactive vs One-Shot + +Use **Interactive Mode** when: +- Building a new application from scratch +- Exploring different design options +- Need to make iterative refinements +- Want AI guidance and suggestions + +Use **One-Shot Generation** when: +- You have a clear, detailed requirements document +- Building a simple, well-defined system +- Automating app generation in scripts +- Need quick prototypes + +### Validation Workflow + +Always validate generated files: + +```bash +# After generation +objectql ai generate -d "..." -o ./src + +# Validate +objectql ai validate ./src -v + +# Auto-fix common issues +objectql ai validate ./src --fix + +# Manually review any remaining issues +``` + +## Environment Variables + +- `OPENAI_API_KEY` - Your OpenAI API key (required) +- `OPENAI_MODEL` - Model to use (optional, default: `gpt-4`) +- `OPENAI_TEMPERATURE` - Generation temperature 0-1 (optional, default: `0.7`) + +## Fallback Behavior + +Without an API key, the CLI will: +- ✅ Still perform basic YAML syntax validation +- ❌ Cannot generate applications +- ❌ Cannot perform AI-powered deep validation +- ❌ Chat assistant unavailable + +```bash +# This still works without API key: +objectql ai validate ./src # Basic YAML syntax check only +``` + +## Next Steps + +- Read [Programmatic API](/ai/programmatic-api) to use AI agent in your code +- Check [Generating Apps](/ai/generating-apps) for advanced prompting techniques +- See [Building Apps](/ai/building-apps) to use ObjectQL in AI applications diff --git a/apps/site/content/docs/ai/coding-assistant.mdx b/apps/site/content/docs/ai/coding-assistant.mdx new file mode 100644 index 00000000..12b73348 --- /dev/null +++ b/apps/site/content/docs/ai/coding-assistant.mdx @@ -0,0 +1,172 @@ +--- +title: AI Coding Assistant Guide +--- + +# AI Coding Assistant Guide + +One of the core design goals of ObjectQL is to be the **most LLM-friendly backend protocol**. + +If you are using **Cursor**, **GitHub Copilot Chat**, **Windsurf**, or **ChatGPT** for development, please copy the following **System Prompt** into your AI configuration or project rules (e.g., `.cursorrules`). +This allows the AI to accurately understand ObjectQL's syntax and best practices. + +## How to Use Effectively + +### 1. "Before" vs "After" + +Without this prompt, Copilot assumes you are using a generic ORM (like TypeORM) and might hallucinate classes: + +> ❌ **Bad AI Output:** +> `await getConnection().manager.find(Todo, { where: { priority: 'high' } })` +> *(ObjectQL doesn't use classes or `getConnection`)* + +With the System Prompt, it understands the **Context + Repository** pattern: + +> ✅ **Good AI Output:** +> `await ctx.object('todo').find({ filters: [['priority', '=', 'high']] })` + +### 2. Prompting Strategy + +When asking the AI to write code, be explicit about the schema you have defined. + +**User Prompt:** +> "Write an API route to complete a todo item. The object is named 'todo' and has a 'completed' boolean field." + +**AI Response (with System Prompt):** +```typescript +app.post('/api/todo/:id/complete', async (req, res) => { + const { id } = req.params; + // Creates a context for the current user (if auth is handled) + const ctx = app.createContext({ userId: req.user?.id }); + + await ctx.object('todo').update(id, { completed: true }); + res.json({ success: true }); +}); +``` + +## Standard System Prompt + +Click the copy button in the top right to get the full prompt: +`text +You are an expert developer specializing in **ObjectQL**, a metadata-driven, low-code backend engine. + +### Core Principles +1. **Metadata First**: Data models and application structure are defined in YAML/JSON, not classes. +2. **Protocol First**: Queries are strict JSON ASTs, not SQL strings. +3. **Instance Naming**: Always name the ObjectQL instance `app`, NEVER `db` (e.g., `const app = new ObjectQL(...)`). +4. **Context-Driven**: All data operations require an execution context (e.g., `const ctx = app.createContext({})`). + +### 1. App Definition (Root) +The application entry point is defined in `.app.yml`. +This file defines the application identity, navigation, and layout. + +Example `todo.app.yml`: +```yaml +kind: app +name: todo_app +label: Todo Application +description: A simple task management app +home_page: /todo +navigation: + - section: Work + items: + - object: todo + - object: project +``` + +### 2. Object Definition (Schema) +Objects are defined in `.object.yml`. +Supported types: `text`, `number`, `boolean`, `date`, `datetime`, `json`, `lookup`, `select`. + +Example `todo.object.yml`: +```yaml +name: todo +label: Todo Item +fields: + title: + type: text + required: true + completed: + type: boolean + defaultValue: false + priority: + type: select + options: [low, medium, high] + owner: + type: lookup + reference_to: user +``` + +### 3. Data Operations (API) +Use the standard generic CRUD API via a context. + +**Query (Find):** +```typescript +const ctx = app.createContext({}); + +const todos = await ctx.object('todo').find({ + filters: [ + ['completed', '=', false], + ['priority', '=', 'high'] + ], + fields: ['title', 'owner.name'], // Select specific fields & relations + sort: [['created_at', 'desc']], + skip: 0, + limit: 20 +}); +``` + +**Mutation (Create/Update/Delete):** +```typescript +const ctx = app.createContext({}); + +// Create +// Returns the ID of the new record or the object itself depending on driver +const newId = await ctx.object('todo').create({ + title: 'Finish ObjectQL Docs', + priority: 'high' +}); + +// Update (by ID) +await ctx.object('todo').update(newId, { + completed: true +}); + +// Delete (by ID) +await ctx.object('todo').delete(newId); +``` + +### 4. Business Logic +Do not write raw logic inside controllers. Use **Hooks** and **Actions**. +All handlers receive a single `context` object. + +**Actions (Registration):** +```typescript +// Register an operation callable by API/Frontend +app.registerAction('todo', 'complete_all', async (ctx) => { + const { input, api, user } = ctx; + // Logic here... + return { success: true }; +}); +``` + +**Hooks (Triggers):** +```typescript +// Valid events: beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, etc. +app.on('beforeCreate', 'todo', async (ctx) => { + // ctx.data contains the payload for create/update + if (!ctx.data.title) { + throw new Error("Title is required"); + } +}); +``` +```` + +--- + +## How to use in tools + +### For Cursor Users +Create a `.cursorrules` file in your project root and paste the content above. Cursor will automatically index these rules. + +### For GitHub Copilot & Others +Add the content to your AI configuration, `.github/copilot-instructions.md`, or paste it into the chat context. diff --git a/apps/site/content/docs/ai/generating-apps.mdx b/apps/site/content/docs/ai/generating-apps.mdx new file mode 100644 index 00000000..4624ed4a --- /dev/null +++ b/apps/site/content/docs/ai/generating-apps.mdx @@ -0,0 +1,115 @@ +--- +title: Generating Apps with AI +--- + +# Generating Apps with AI + +Because ObjectQL applications are defined primarily by **Metadata** (YAML/JSON) rather than **Code** (Classes/Functions), LLMs can generate complete, working backends in a single pass. + +This is "Zero-Shot Microservice Generation". + +## The Workflow + +1. **Prompt**: You ask the AI to design a system (e.g., "A Property Management System"). +2. **Generation**: The AI outputs a set of `*.object.yml` files. +3. **Execution**: You drop these files into your project, and the API is instantly live. + +## Prompting Guide + +To get the best results, use a prompt that enforces the ObjectQL schema format. + +### Template + +> **Act as an ObjectQL Architect.** +> +> I need a backend for **[APP_DESCRIPTION]**. +> +> Please generate the object definitions in YAML format. +> Follow these rules: +> 1. Use standard types: `text`, `number`, `boolean`, `select`, `date`, `lookup`. +> 2. For relationships, use `type: lookup` with `reference_to: `. +> 3. Include `required: true` for important fields. +> 4. Suggest 2-3 essential objects. +> +> Output the YAML for each object in a separate code block. + +### Example: Real Estate CRM + +**User:** +> "I need a backend for a Real Estate Agency to track Properties and Agents." + +**AI Output:** + +```yaml +# agent.object.yml +name: agent +fields: + name: + type: text + required: true + email: + type: email + required: true + phone: + type: phone + license_number: + type: text + status: + type: select + options: [active, inactive] +``` + +```yaml +# property.object.yml +name: property +fields: + address: + type: textarea + required: true + price: + type: currency + bedrooms: + type: number + bathrooms: + type: number + listing_agent: + type: lookup + reference_to: agent + status: + type: select + options: [for_sale, sold, pending] +``` + +## Automating the Process + +You can build tools to automate this loop. + +### 1. The Generator Script +Imagine a simple CLI tool that takes a user description and writes files to disk. + +```typescript +import { generateSchema } from './my-llm-service'; // Wrapper around OpenAI + +async function main() { + const description = process.argv[2]; + const schemas = await generateSchema(description); + + for (const schema of schemas) { + fs.writeFileSync(`${schema.name}.object.yml`, yaml.dump(schema)); + } + + console.log("App generated! Starting server..."); +} +``` + +### 2. Hot Reloading +Since ObjectQL can load metadata at runtime, you can build **Self-Evolving Apps**. +1. The App receives a request: "Add a 'renovation_date' field to Property." +2. The App calls an LLM to update the YAML. +3. The App reloads the metadata registry. +4. The new field is immediately available via API. + +## Summary + +ObjectQL turns software development into a **Content Generation** task. +Instead of generating complex imperative code (which is brittle), you generate simple declarative configurations (which are robust). diff --git a/apps/site/content/docs/ai/index.mdx b/apps/site/content/docs/ai/index.mdx new file mode 100644 index 00000000..1ecd0c4a --- /dev/null +++ b/apps/site/content/docs/ai/index.mdx @@ -0,0 +1,107 @@ +--- +title: AI-Native Development +--- + +# AI-Native Development + +ObjectQL resides at the intersection of Data and Artificial Intelligence. It is designed to be "AI-Native" in two distinct ways: + +1. **For AI Agents**: It serves as the perfect structured memory system (Long-term Memory) and tool interface for AI agents. +2. **By AI Agents**: Its metadata-driven, protocol-first architecture makes it incredibly easy for LLMs to write valid code for it. + +## Overview + +### 🤖 AI-Powered CLI +Use the `objectql ai` command to generate complete applications from natural language, validate metadata, and get interactive assistance. + +* [CLI Usage Guide](/ai/cli-usage) +* [Interactive Mode](/ai/cli-usage#interactive-mode-default) +* [One-Shot Generation](/ai/cli-usage#one-shot-generation) +* [Validation](/ai/cli-usage#validation) + +[Read CLI Guide →](/ai/cli-usage) + +### 📚 Programmatic API +Use the ObjectQL AI Agent in your Node.js applications to build custom development tools, web UIs, and automation. + +* [Basic Usage](/ai/programmatic-api#basic-usage) +* [TypeScript Types](/ai/programmatic-api#typescript-types) +* [Advanced Examples](/ai/programmatic-api#advanced-examples) + +[Read API Docs →](/ai/programmatic-api) + +### ✨ Generating Apps +Turn natural language into full backend systems instantly. Because ObjectQL uses declarative YAML/JSON, LLMs can "write" software by simply generating configuration files. + +* [Zero-Shot Generation](/ai/generating-apps) +* [Prompt Templates](/ai/generating-apps#prompting-guide) + +[Start Generating →](/ai/generating-apps) + +### 🏗️ Building AI Apps +Learn how to use ObjectQL as the data engine for your RAG (Retrieval-Augmented Generation) applications, semantic search, and AI agents. + +* [Semantic Search & RAG](/ai/building-apps#semantic-search-rag) +* [Vector Database Features](/ai/building-apps#configuration) +* [Agent Tooling](/ai/building-apps) + +[Read Guide →](/ai/building-apps) + +### 🤖 AI Coding Assistant +Learn how to configure GitHub Copilot, Cursor, or Windsurf to become an expert ObjectQL developer. We provide specialized System Prompts that teach the LLM our protocol. + +* [Standard System Prompt](/ai/coding-assistant#standard-system-prompt-system-prompt) +* [IDE Configuration](/ai/coding-assistant#how-to-use-in-tools) + +[Get Prompts →](/ai/coding-assistant) + +## Quick Start + +### Command Line + +```bash +# Set your API key +export OPENAI_API_KEY=sk-your-key + +# Start interactive mode (easiest!) +objectql ai + +# Or one-shot generation +objectql ai generate -d "A CRM system with customers and contacts" + +# Validate metadata +objectql ai validate ./src +``` + +### Programmatic + +```typescript +import { ObjectQLAgent } from '@objectql/core'; + +const agent = new ObjectQLAgent({ + apiKey: process.env.OPENAI_API_KEY! +}); + +// Generate application +const result = await agent.generateApp({ + description: 'Project management with tasks and milestones', + type: 'complete' +}); + +// Validate metadata +const validation = await agent.validateMetadata({ + metadata: yamlContent, + checkBusinessLogic: true +}); +``` + +## Key Features + +✅ **Natural Language to Code** - Describe your app, get complete metadata +✅ **TypeScript Generation** - Actions and hooks with full implementations +✅ **Test Generation** - Automatic Jest tests for business logic +✅ **AI-Powered Validation** - Deep analysis beyond syntax checking +✅ **Interactive Building** - Conversational refinement through dialogue +✅ **Programmatic API** - Build custom dev tools and automation +✅ **Multi-Language Support** - Works with English and Chinese prompts +✅ **Specification Compliance** - Ensures generated code follows ObjectQL standards diff --git a/apps/site/content/docs/ai/programmatic-api.mdx b/apps/site/content/docs/ai/programmatic-api.mdx new file mode 100644 index 00000000..9b9e5061 --- /dev/null +++ b/apps/site/content/docs/ai/programmatic-api.mdx @@ -0,0 +1,513 @@ +--- +title: Programmatic AI API +--- + +# Programmatic AI API + +The ObjectQL AI Agent provides a programmatic API for generating and validating applications in your Node.js code. + +## Overview + +The AI Agent is available in `@objectql/core` package and can be used to: +- Generate applications from natural language +- Validate metadata programmatically +- Build interactive application builders +- Create AI-powered development tools + +## Installation + +The AI Agent is part of the core package: + +```bash +npm install @objectql/core +``` + +## Basic Usage + +### Creating an Agent + +```typescript +import { ObjectQLAgent } from '@objectql/core'; + +const agent = new ObjectQLAgent({ + apiKey: process.env.OPENAI_API_KEY!, + model: process.env.OPENAI_MODEL || 'gpt-4', // optional, default: 'gpt-4' + temperature: 0.7, // optional, default: 0.7 + language: 'en' // optional, default: 'en' +}); +``` + +### Generating Applications + +```typescript +const result = await agent.generateApp({ + description: 'A task management system with projects and tasks', + type: 'complete', // 'basic' | 'complete' | 'custom' + maxTokens: 4000 // optional +}); + +if (result.success) { + console.log(`Generated ${result.files.length} files:`); + + for (const file of result.files) { + console.log(`- ${file.filename} (${file.type})`); + + // Write to disk + fs.writeFileSync( + path.join('./src', file.filename), + file.content + ); + } +} else { + console.error('Errors:', result.errors); +} +``` + +### Validating Metadata + +```typescript +const yamlContent = fs.readFileSync('./user.object.yml', 'utf8'); + +const validation = await agent.validateMetadata({ + metadata: yamlContent, + filename: 'user.object.yml', + checkBusinessLogic: true, + checkPerformance: true, + checkSecurity: true +}); + +if (!validation.valid) { + console.log('Errors:'); + validation.errors.forEach(err => { + console.log(` - ${err.message} (${err.location})`); + }); +} + +if (validation.warnings.length > 0) { + console.log('Warnings:'); + validation.warnings.forEach(warn => { + console.log(` - ${warn.message}`); + if (warn.suggestion) { + console.log(` Suggestion: ${warn.suggestion}`); + } + }); +} +``` + +### Interactive Conversational Generation + +Build applications through multi-turn conversation: + +```typescript +let conversationHistory: ConversationMessage[] = []; +let currentApp: GenerateAppResult | undefined; + +// First turn +const result1 = await agent.generateConversational({ + message: 'Create a blog system with posts and comments', + conversationHistory, + currentApp +}); + +conversationHistory = result1.conversationHistory; +currentApp = result1; + +console.log('Generated files:', result1.files.map(f => f.filename)); +console.log('Suggestions:', result1.suggestions); + +// Second turn - refine based on user feedback +const result2 = await agent.generateConversational({ + message: 'Add tags to posts and categories for organization', + conversationHistory, + currentApp +}); + +conversationHistory = result2.conversationHistory; +currentApp = result2; + +console.log('Updated files:', result2.files.map(f => f.filename)); +``` + +### Refining Metadata + +Iteratively improve metadata based on feedback: + +```typescript +const initialMetadata = ` +label: User +fields: + name: + type: string # Wrong - should be 'text' + email: + type: text +`; + +const refined = await agent.refineMetadata( + initialMetadata, + 'Fix field types and add email validation', + 2 // number of refinement iterations +); + +console.log('Refined metadata:', refined.files[0].content); +``` + +## TypeScript Types + +### AgentConfig + +```typescript +interface AgentConfig { + /** OpenAI API key */ + apiKey: string; + /** OpenAI model to use (default: gpt-4) */ + model?: string; + /** Temperature for generation (0-1, default: 0.7) */ + temperature?: number; + /** Preferred language for messages (default: en) */ + language?: string; +} +``` + +### GenerateAppOptions + +```typescript +interface GenerateAppOptions { + /** Natural language description of the application */ + description: string; + /** Type of generation: basic (minimal), complete (comprehensive), or custom */ + type?: 'basic' | 'complete' | 'custom'; + /** Maximum tokens for generation */ + maxTokens?: number; +} +``` + +### GenerateAppResult + +```typescript +interface GenerateAppResult { + /** Whether generation was successful */ + success: boolean; + /** Generated metadata files */ + files: Array<{ + filename: string; + content: string; + type: 'object' | 'validation' | 'form' | 'view' | 'page' | + 'menu' | 'action' | 'hook' | 'permission' | 'workflow' | + 'report' | 'data' | 'application' | 'typescript' | 'test' | 'other'; + }>; + /** Any errors encountered */ + errors?: string[]; + /** AI model response (raw) */ + rawResponse?: string; +} +``` + +### ValidateMetadataOptions + +```typescript +interface ValidateMetadataOptions { + /** Metadata content (YAML string or parsed object) */ + metadata: string | any; + /** Filename (for context) */ + filename?: string; + /** Whether to check business logic consistency */ + checkBusinessLogic?: boolean; + /** Whether to check performance considerations */ + checkPerformance?: boolean; + /** Whether to check security issues */ + checkSecurity?: boolean; +} +``` + +### ValidateMetadataResult + +```typescript +interface ValidateMetadataResult { + /** Whether validation passed (no errors) */ + valid: boolean; + /** Errors found */ + errors: Array<{ + message: string; + location?: string; + code?: string; + }>; + /** Warnings found */ + warnings: Array<{ + message: string; + location?: string; + suggestion?: string; + }>; + /** Informational messages */ + info: Array<{ + message: string; + location?: string; + }>; +} +``` + +### ConversationMessage + +```typescript +interface ConversationMessage { + role: 'system' | 'user' | 'assistant'; + content: string; +} +``` + +### ConversationalGenerateOptions + +```typescript +interface ConversationalGenerateOptions { + /** Initial description or follow-up request */ + message: string; + /** Previous conversation history */ + conversationHistory?: ConversationMessage[]; + /** Current application state (already generated files) */ + currentApp?: GenerateAppResult; +} +``` + +### ConversationalGenerateResult + +```typescript +interface ConversationalGenerateResult extends GenerateAppResult { + /** Updated conversation history */ + conversationHistory: ConversationMessage[]; + /** Suggested next steps or questions */ + suggestions?: string[]; +} +``` + +## Advanced Examples + +### Building a Web UI for App Generation + +```typescript +import express from 'express'; +import { ObjectQLAgent } from '@objectql/core'; + +const app = express(); +const agent = new ObjectQLAgent({ apiKey: process.env.OPENAI_API_KEY! }); + +// Store conversations per session +const sessions = new Map(); + +app.post('/api/generate', async (req, res) => { + const { sessionId, message, currentApp } = req.body; + + const conversationHistory = sessions.get(sessionId) || []; + + const result = await agent.generateConversational({ + message, + conversationHistory, + currentApp + }); + + sessions.set(sessionId, result.conversationHistory); + + res.json(result); +}); + +app.listen(3000); +``` + +### Automated Testing of Generated Apps + +```typescript +import { ObjectQLAgent } from '@objectql/core'; +import { ObjectQL } from '@objectql/core'; + +async function generateAndTest(description: string) { + const agent = new ObjectQLAgent({ apiKey: process.env.OPENAI_API_KEY! }); + + // Generate app + const result = await agent.generateApp({ + description, + type: 'complete' + }); + + if (!result.success) { + throw new Error('Generation failed'); + } + + // Write files + for (const file of result.files) { + fs.writeFileSync(`./test-app/${file.filename}`, file.content); + } + + // Validate all metadata + for (const file of result.files.filter(f => f.filename.endsWith('.yml'))) { + const validation = await agent.validateMetadata({ + metadata: file.content, + filename: file.filename, + checkBusinessLogic: true, + checkSecurity: true + }); + + if (!validation.valid) { + console.error(`Validation failed for ${file.filename}:`, validation.errors); + } + } + + // Start ObjectQL instance and test + const objectql = new ObjectQL({ + metadataPath: './test-app', + driver: 'sqlite', + connection: { filename: ':memory:' } + }); + + await objectql.connect(); + + // Run tests + // ... + + await objectql.disconnect(); +} +``` + +### CI/CD Integration + +```typescript +// In your CI pipeline +import { ObjectQLAgent } from '@objectql/core'; + +async function validateAllMetadata(metadataDir: string): Promise { + const agent = new ObjectQLAgent({ apiKey: process.env.OPENAI_API_KEY! }); + + const files = glob.sync(`${metadataDir}/**/*.yml`); + let hasErrors = false; + + for (const file of files) { + const content = fs.readFileSync(file, 'utf8'); + + const result = await agent.validateMetadata({ + metadata: content, + filename: path.basename(file), + checkBusinessLogic: true, + checkSecurity: true, + checkPerformance: true + }); + + if (!result.valid) { + console.error(`❌ ${file}:`); + result.errors.forEach(e => console.error(` ${e.message}`)); + hasErrors = true; + } + + result.warnings.forEach(w => { + console.warn(`⚠️ ${file}: ${w.message}`); + }); + } + + return !hasErrors; +} + +// In GitHub Actions workflow +const success = await validateAllMetadata('./src/metadata'); +process.exit(success ? 0 : 1); +``` + +### Custom Metadata Generator + +```typescript +class CustomAppGenerator { + private agent: ObjectQLAgent; + + constructor(apiKey: string) { + this.agent = new ObjectQLAgent({ apiKey }); + } + + async generateFromTemplate( + template: string, + variables: Record + ): Promise { + // Replace variables in template + let description = template; + for (const [key, value] of Object.entries(variables)) { + description = description.replace(`{{${key}}}`, value); + } + + // Generate + const result = await this.agent.generateApp({ + description, + type: 'complete' + }); + + // Post-process files + if (result.success) { + result.files = result.files.map(file => ({ + ...file, + content: this.postProcess(file.content, variables) + })); + } + + return result; + } + + private postProcess(content: string, variables: Record): string { + // Custom post-processing logic + return content; + } +} + +// Usage +const generator = new CustomAppGenerator(process.env.OPENAI_API_KEY!); + +const result = await generator.generateFromTemplate( + 'A {{industry}} management system with {{entities}}', + { + industry: 'healthcare', + entities: 'patients, appointments, and medical records' + } +); +``` + +## Error Handling + +Always handle errors when using the AI Agent: + +```typescript +try { + const result = await agent.generateApp({ + description: 'My application' + }); + + if (!result.success) { + // Handle generation failure + console.error('Generation failed:', result.errors); + + // You might want to retry or provide feedback to user + if (result.rawResponse) { + console.log('Raw response:', result.rawResponse); + } + } + +} catch (error) { + // Handle API errors (network, auth, etc.) + if (error instanceof Error) { + console.error('API error:', error.message); + } +} +``` + +## Best Practices + +1. **API Key Security**: Never hardcode API keys. Use environment variables. + +2. **Rate Limiting**: Implement rate limiting when exposing the agent in a web API. + +3. **Caching**: Cache generation results to avoid redundant API calls. + +4. **Validation**: Always validate generated metadata before using in production. + +5. **Error Recovery**: Implement retry logic with exponential backoff for API failures. + +6. **Type Safety**: Use TypeScript for type safety with the agent API. + +7. **Testing**: Test generated applications thoroughly before deployment. + +## Next Steps + +- See [CLI Usage](/ai/cli-usage) for command-line tools +- Read [Generating Apps](/ai/generating-apps) for prompting best practices +- Check [Building Apps](/ai/building-apps) for using ObjectQL in AI applications diff --git a/apps/site/content/docs/api/attachments.mdx b/apps/site/content/docs/api/attachments.mdx new file mode 100644 index 00000000..e5cb1d01 --- /dev/null +++ b/apps/site/content/docs/api/attachments.mdx @@ -0,0 +1,1250 @@ +--- +title: Attachment API Specification +--- + +# Attachment API Specification + +**Version:** 1.0.0 + +This document specifies how to handle file uploads, image uploads, and attachment field types in ObjectQL APIs. + +> **💡 Quick Guides:** +> - **How to upload multiple files to one field?** → [Multiple File Upload Guide (中文)](../examples/multiple-file-upload-guide-cn.md) ⭐ +> - **How to associate attachments with records?** → [Attachment Association Guide (中文)](../examples/attachment-association-guide-cn.md) +> - **How to integrate S3 storage?** → [S3 Integration Guide (中文)](../examples/s3-integration-guide-cn.md) + +## Table of Contents + +1. [Overview](#overview) +2. [Field Types](#field-types) +3. [Data Format](#data-format) +4. [Upload API](#upload-api) +5. [CRUD Operations with Attachments](#crud-operations-with-attachments) +6. [Download & Access](#download--access) +7. [Best Practices](#best-practices) +8. [Examples](#examples) + +--- + +## Overview + +ObjectQL supports two attachment-related field types: + +- **`file`**: General file attachments (documents, PDFs, archives, etc.) +- **`image`**: Image files with optional image-specific metadata (including user avatars, product photos, galleries, etc.) + +All attachment fields store metadata as JSON in the database, while the actual file content is stored in a configurable storage backend (local filesystem, S3, cloud storage, etc.). + +**Note:** User profile pictures (avatars) should use the `image` type with appropriate constraints (e.g., `multiple: false`, size limits, aspect ratio requirements). UI frameworks can identify avatar fields by naming conventions (e.g., `profile_picture`, `avatar`) to apply specific rendering (circular cropping, etc.). + +## Design Principles + +1. **Metadata-Driven**: File metadata (URL, size, type) is stored in the database +2. **Storage-Agnostic**: Actual files can be stored anywhere (local, S3, CDN) +3. **URL-Based**: Files are referenced by URLs for maximum flexibility +4. **Type-Safe**: Full TypeScript support for attachment data structures +5. **Validation**: Built-in file type, size, and extension validation + +--- + +## Field Types + +## `file` Field Type + +General-purpose file attachment field. + +**Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `type` | `'file'` | **Required.** Field type identifier | +| `label` | `string` | Display label | +| `required` | `boolean` | Whether file is mandatory | +| `multiple` | `boolean` | Allow multiple file uploads (array) | +| `accept` | `string[]` | Allowed file extensions (e.g., `['.pdf', '.docx']`) | +| `max_size` | `number` | Maximum file size in bytes | +| `min_size` | `number` | Minimum file size in bytes | + +**Example Definition:** + +```yaml +# expense.object.yml +fields: + receipt: + type: file + label: Receipt Attachment + required: true + accept: ['.pdf', '.jpg', '.png'] + max_size: 5242880 # 5MB + + supporting_docs: + type: file + label: Supporting Documents + multiple: true + accept: ['.pdf', '.docx', '.xlsx'] + max_size: 10485760 # 10MB +``` + +## `image` Field Type + +Image-specific attachment field with additional metadata support. + +**Properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `type` | `'image'` | **Required.** Field type identifier | +| `label` | `string` | Display label | +| `required` | `boolean` | Whether image is mandatory | +| `multiple` | `boolean` | Allow multiple images (gallery) | +| `accept` | `string[]` | Allowed image formats (default: `['.jpg', '.jpeg', '.png', '.gif', '.webp']`) | +| `max_size` | `number` | Maximum file size in bytes | +| `max_width` | `number` | Maximum image width in pixels | +| `max_height` | `number` | Maximum image height in pixels | +| `min_width` | `number` | Minimum image width in pixels | +| `min_height` | `number` | Minimum image height in pixels | + +**Example Definition:** + +```yaml +# product.object.yml +fields: + product_image: + type: image + label: Product Image + required: true + accept: ['.jpg', '.png', '.webp'] + max_size: 2097152 # 2MB + max_width: 2000 + max_height: 2000 + + gallery: + type: image + label: Product Gallery + multiple: true + max_size: 5242880 # 5MB per image + + # User avatar (profile picture) + profile_picture: + type: image + label: Profile Picture + multiple: false # Single image only + max_size: 1048576 # 1MB + max_width: 500 + max_height: 500 + accept: ['.jpg', '.png', '.webp'] +``` + +--- + +## Data Format + +Attachment fields store structured JSON data containing file metadata. The actual file content is stored separately. + +## Single File Format + +For non-multiple file/image fields, the data is stored as a single object: + +```typescript +interface AttachmentData { + /** Unique identifier for this file */ + id?: string; + + /** File name (e.g., "invoice.pdf") */ + name: string; + + /** Publicly accessible URL to the file */ + url: string; + + /** File size in bytes */ + size: number; + + /** MIME type (e.g., "application/pdf", "image/jpeg") */ + type: string; + + /** Original filename as uploaded by user */ + original_name?: string; + + /** Upload timestamp (ISO 8601) */ + uploaded_at?: string; + + /** User ID who uploaded the file */ + uploaded_by?: string; +} +``` + +**Example:** + +```json +{ + "id": "file_abc123", + "name": "receipt_2024.pdf", + "url": "https://cdn.example.com/files/receipt_2024.pdf", + "size": 245760, + "type": "application/pdf", + "original_name": "Receipt - Jan 2024.pdf", + "uploaded_at": "2024-01-15T10:30:00Z", + "uploaded_by": "user_xyz" +} +``` + +## Multiple Files Format + +For `multiple: true` fields, the data is an array of attachment objects: + +```typescript +type MultipleAttachmentData = AttachmentData[]; +``` + +**Example:** + +```json +[ + { + "id": "img_001", + "name": "product_front.jpg", + "url": "https://cdn.example.com/images/product_front.jpg", + "size": 156789, + "type": "image/jpeg", + "uploaded_at": "2024-01-15T10:30:00Z" + }, + { + "id": "img_002", + "name": "product_back.jpg", + "url": "https://cdn.example.com/images/product_back.jpg", + "size": 142356, + "type": "image/jpeg", + "uploaded_at": "2024-01-15T10:31:00Z" + } +] +``` + +## Image-Specific Metadata + +Image fields can include additional metadata: + +```typescript +interface ImageAttachmentData extends AttachmentData { + /** Image width in pixels */ + width?: number; + + /** Image height in pixels */ + height?: number; + + /** Thumbnail URL (if generated) */ + thumbnail_url?: string; + + /** Alternative sizes/versions */ + variants?: { + small?: string; + medium?: string; + large?: string; + }; +} +``` + +**Example:** + +```json +{ + "id": "img_abc123", + "name": "product_hero.jpg", + "url": "https://cdn.example.com/images/product_hero.jpg", + "size": 523400, + "type": "image/jpeg", + "width": 1920, + "height": 1080, + "thumbnail_url": "https://cdn.example.com/images/product_hero_thumb.jpg", + "variants": { + "small": "https://cdn.example.com/images/product_hero_small.jpg", + "medium": "https://cdn.example.com/images/product_hero_medium.jpg", + "large": "https://cdn.example.com/images/product_hero_large.jpg" + } +} +``` + +--- + +## Upload API + +ObjectQL provides dedicated endpoints for file uploads using multipart/form-data. + +## Upload Endpoint + +``` +POST /api/files/upload +Content-Type: multipart/form-data +Authorization: Bearer +``` + +## Request Format + +Use standard multipart form data with the following fields: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file` | File | Yes | The file to upload | +| `object` | string | No | Object name (for context/validation) | +| `field` | string | No | Field name (for validation against field config) | +| `folder` | string | No | Logical folder/path for organization | + +## Response Format + +**Success Response (200 OK):** + +```json +{ + "data": { + "id": "file_abc123", + "name": "invoice.pdf", + "url": "https://cdn.example.com/files/invoice.pdf", + "size": 245760, + "type": "application/pdf", + "uploaded_at": "2024-01-15T10:30:00Z", + "uploaded_by": "user_xyz" + } +} +``` + +**Error Response (400 Bad Request):** + +```json +{ + "error": { + "code": "FILE_VALIDATION_ERROR", + "message": "File validation failed", + "details": { + "file": "invoice.exe", + "reason": "File type not allowed. Allowed types: .pdf, .jpg, .png" + } + } +} +``` + +## Upload Examples + +**Using cURL:** + +```bash +curl -X POST https://api.example.com/api/files/upload \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -F "file=@/path/to/invoice.pdf" \ + -F "object=expense" \ + -F "field=receipt" +``` + +**Using JavaScript Fetch:** + +```javascript +const fileInput = document.getElementById('fileInput'); +const file = fileInput.files[0]; + +const formData = new FormData(); +formData.append('file', file); +formData.append('object', 'expense'); +formData.append('field', 'receipt'); + +const response = await fetch('/api/files/upload', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token + }, + body: formData +}); + +const { data: uploadedFile } = await response.json(); +console.log('Uploaded:', uploadedFile); +// { id: 'file_abc123', url: '...', ... } +``` + +**Using Axios:** + +```javascript +import axios from 'axios'; + +const formData = new FormData(); +formData.append('file', file); +formData.append('object', 'expense'); +formData.append('field', 'receipt'); + +const response = await axios.post('/api/files/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + 'Authorization': 'Bearer ' + token + } +}); + +const uploadedFile = response.data.data; +``` + +## Batch Upload + +For uploading multiple files at once: + +``` +POST /api/files/upload/batch +Content-Type: multipart/form-data +``` + +**Request:** + +```bash +curl -X POST https://api.example.com/api/files/upload/batch \ + -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + -F "files=@/path/to/image1.jpg" \ + -F "files=@/path/to/image2.jpg" \ + -F "files=@/path/to/image3.jpg" \ + -F "object=product" \ + -F "field=gallery" +``` + +**Response:** + +```json +{ + "data": [ + { + "id": "img_001", + "name": "image1.jpg", + "url": "https://cdn.example.com/images/image1.jpg", + "size": 156789, + "type": "image/jpeg" + }, + { + "id": "img_002", + "name": "image2.jpg", + "url": "https://cdn.example.com/images/image2.jpg", + "size": 142356, + "type": "image/jpeg" + }, + { + "id": "img_003", + "name": "image3.jpg", + "url": "https://cdn.example.com/images/image3.jpg", + "size": 198234, + "type": "image/jpeg" + } + ] +} +``` + +--- + +## CRUD Operations with Attachments + +## Creating Records with Attachments + +**Step 1: Upload the file(s)** + +```javascript +// Upload file first +const uploadResponse = await fetch('/api/files/upload', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + token }, + body: formData +}); + +const uploadedFile = (await uploadResponse.json()).data; +``` + +**Step 2: Create record with file metadata** + +```javascript +// Create expense record with the uploaded file +const createResponse = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + expense_number: 'EXP-2024-001', + amount: 125.50, + description: 'Office supplies', + receipt: uploadedFile // Reference to uploaded file + } + }) +}); + +const expense = (await createResponse.json()).data; +``` + +**Complete Example (JSON-RPC):** + +```json +{ + "op": "create", + "object": "expense", + "args": { + "expense_number": "EXP-2024-001", + "amount": 125.50, + "category": "office_supplies", + "description": "Office supplies - printer paper", + "receipt": { + "id": "file_abc123", + "name": "receipt.pdf", + "url": "https://cdn.example.com/files/receipt.pdf", + "size": 245760, + "type": "application/pdf" + } + } +} +``` + +## Creating with Multiple Attachments + +```json +{ + "op": "create", + "object": "product", + "args": { + "name": "Premium Laptop", + "price": 1299.99, + "description": "High-performance laptop", + "gallery": [ + { + "id": "img_001", + "name": "laptop_front.jpg", + "url": "https://cdn.example.com/images/laptop_front.jpg", + "size": 156789, + "type": "image/jpeg", + "width": 1920, + "height": 1080 + }, + { + "id": "img_002", + "name": "laptop_back.jpg", + "url": "https://cdn.example.com/images/laptop_back.jpg", + "size": 142356, + "type": "image/jpeg", + "width": 1920, + "height": 1080 + } + ] + } +} +``` + +## Updating Attachments + +**Replace entire attachment:** + +```json +{ + "op": "update", + "object": "expense", + "args": { + "id": "exp_xyz", + "data": { + "receipt": { + "id": "file_new123", + "name": "updated_receipt.pdf", + "url": "https://cdn.example.com/files/updated_receipt.pdf", + "size": 198234, + "type": "application/pdf" + } + } + } +} +``` + +**Add to multiple attachments (array):** + +```javascript +// First, fetch the current record +const currentRecord = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'findOne', + object: 'product', + args: 'product_123' + }) +}).then(r => r.json()); + +// Upload new image +const newImage = await uploadFile(file); + +// Update with appended gallery +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'update', + object: 'product', + args: { + id: 'product_123', + data: { + gallery: [...currentRecord.data.gallery, newImage] + } + } + }) +}); +``` + +**Remove attachment:** + +```json +{ + "op": "update", + "object": "expense", + "args": { + "id": "exp_xyz", + "data": { + "receipt": null + } + } +} +``` + +## Querying Records with Attachments + +Attachments are returned as part of the record data: + +```json +{ + "op": "find", + "object": "expense", + "args": { + "fields": ["id", "expense_number", "amount", "receipt"], + "filters": [["status", "=", "approved"]] + } +} +``` + +**Response:** + +```json +{ + "data": [ + { + "id": "exp_001", + "expense_number": "EXP-2024-001", + "amount": 125.50, + "receipt": { + "id": "file_abc123", + "name": "receipt.pdf", + "url": "https://cdn.example.com/files/receipt.pdf", + "size": 245760, + "type": "application/pdf" + } + }, + { + "id": "exp_002", + "expense_number": "EXP-2024-002", + "amount": 89.99, + "receipt": null // No receipt attached + } + ] +} +``` + +## Filtering by Attachment Presence + +Check if a file is attached: + +```json +{ + "op": "find", + "object": "expense", + "args": { + "filters": [["receipt", "!=", null]] + } +} +``` + +Check if no file is attached: + +```json +{ + "op": "find", + "object": "expense", + "args": { + "filters": [["receipt", "=", null]] + } +} +``` + +--- + +## Download & Access + +## Direct URL Access + +Files are accessed directly via their `url` property: + +```javascript +const expense = await fetchExpense('exp_123'); +const receiptUrl = expense.receipt.url; + +// Download file +window.open(receiptUrl, '_blank'); + +// Or display image +document.getElementById('receipt-img').src = receiptUrl; +``` + +## Secure Download Endpoint + +For files requiring authentication: + +``` +GET /api/files/:fileId +Authorization: Bearer +``` + +**Example:** + +```bash +curl -H "Authorization: Bearer YOUR_JWT_TOKEN" \ + https://api.example.com/api/files/file_abc123 \ + --output receipt.pdf +``` + +## Thumbnail/Preview Endpoint + +For images, request specific sizes: + +``` +GET /api/files/:fileId/thumbnail?size=small|medium|large +GET /api/files/:fileId/preview?width=200&height=200 +``` + +**Example:** + +```html + +Product + + +Product +``` + +--- + +## Best Practices + +## Security + +1. **Validate File Types**: Always specify `accept` to restrict file types +2. **Enforce Size Limits**: Set appropriate `max_size` to prevent abuse +3. **Scan for Malware**: Integrate virus scanning for uploaded files +4. **Use Signed URLs**: For sensitive files, use time-limited signed URLs +5. **Authenticate Downloads**: Require authentication for private files + +## Performance + +1. **Use CDN**: Store files on CDN for fast global access +2. **Generate Thumbnails**: Pre-generate image thumbnails for galleries +3. **Lazy Load Images**: Load images on-demand in lists +4. **Compress Images**: Automatically compress uploaded images +5. **Cache Metadata**: Cache file metadata to reduce database queries + +## Storage + +1. **Organize by Object**: Store files in folders by object type +2. **Use Object Storage**: Use S3, GCS, or Azure Blob for scalability +3. **Implement Cleanup**: Delete orphaned files periodically +4. **Version Files**: Keep file versions for audit trails +5. **Backup Regularly**: Include files in backup strategy + +## User Experience + +1. **Show Upload Progress**: Display progress bars for large files +2. **Preview Before Upload**: Show image previews before submission +3. **Validate Client-Side**: Check file type/size before upload +4. **Provide Feedback**: Clear error messages for upload failures +5. **Support Drag & Drop**: Enable drag-and-drop file upload + +--- + +## Examples + +## Complete Upload & Create Flow + +```javascript +/** + * Upload a file and create an expense record + */ +async function createExpenseWithReceipt(expenseData, receiptFile) { + // Step 1: Upload the receipt file + const formData = new FormData(); + formData.append('file', receiptFile); + formData.append('object', 'expense'); + formData.append('field', 'receipt'); + + const uploadResponse = await fetch('/api/files/upload', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: formData + }); + + if (!uploadResponse.ok) { + throw new Error('File upload failed'); + } + + const uploadedFile = (await uploadResponse.json()).data; + + // Step 2: Create expense record with file metadata + const createResponse = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + ...expenseData, + receipt: uploadedFile + } + }) + }); + + if (!createResponse.ok) { + throw new Error('Failed to create expense'); + } + + return (await createResponse.json()).data; +} + +// Usage +const file = document.getElementById('receipt-input').files[0]; +const expense = await createExpenseWithReceipt({ + expense_number: 'EXP-2024-001', + amount: 125.50, + category: 'office_supplies', + description: 'Printer paper and toner' +}, file); + +console.log('Created expense:', expense); +``` + +## Product Gallery Management + +```javascript +/** + * Upload multiple images and create product + */ +async function createProductWithGallery(productData, imageFiles) { + // Upload all images + const uploadPromises = Array.from(imageFiles).map(async (file) => { + const formData = new FormData(); + formData.append('file', file); + formData.append('object', 'product'); + formData.append('field', 'gallery'); + + const response = await fetch('/api/files/upload', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + getAuthToken() }, + body: formData + }); + + return (await response.json()).data; + }); + + const uploadedImages = await Promise.all(uploadPromises); + + // Create product with gallery + const response = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: JSON.stringify({ + op: 'create', + object: 'product', + args: { + ...productData, + gallery: uploadedImages + } + }) + }); + + return (await response.json()).data; +} + +// Usage +const files = document.getElementById('gallery-input').files; +const product = await createProductWithGallery({ + name: 'Premium Laptop', + price: 1299.99, + description: 'High-performance laptop' +}, files); +``` + +## Update User Avatar + +```javascript +/** + * Update user profile picture + */ +async function updateUserAvatar(userId, avatarFile) { + // Upload avatar + const formData = new FormData(); + formData.append('file', avatarFile); + formData.append('object', 'user'); + formData.append('field', 'profile_picture'); + + const uploadResponse = await fetch('/api/files/upload', { + method: 'POST', + headers: { 'Authorization': 'Bearer ' + getAuthToken() }, + body: formData + }); + + const uploadedAvatar = (await uploadResponse.json()).data; + + // Update user record + const updateResponse = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: JSON.stringify({ + op: 'update', + object: 'user', + args: { + id: userId, + data: { + profile_picture: uploadedAvatar + } + } + }) + }); + + return (await updateResponse.json()).data; +} + +// Usage +const avatarFile = document.getElementById('avatar-input').files[0]; +const updatedUser = await updateUserAvatar('user_123', avatarFile); +``` + +## React Component Example + +```typescript +import React, { useState } from 'react'; +import { ObjectQLClient } from '@objectql/sdk'; + +interface UploadReceiptProps { + expenseId?: string; + onSuccess?: (expense: any) => void; +} + +export const UploadReceipt: React.FC = ({ + expenseId, + onSuccess +}) => { + const [uploading, setUploading] = useState(false); + const [error, setError] = useState(null); + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Validate file + if (!file.type.match(/^(application\/pdf|image\/(jpeg|png))$/)) { + setError('Only PDF, JPG, and PNG files are allowed'); + return; + } + + if (file.size > 5 * 1024 * 1024) { + setError('File size must be less than 5MB'); + return; + } + + setUploading(true); + setError(null); + + try { + // Upload file + const formData = new FormData(); + formData.append('file', file); + formData.append('object', 'expense'); + formData.append('field', 'receipt'); + + const uploadResponse = await fetch('/api/files/upload', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: formData + }); + + if (!uploadResponse.ok) { + throw new Error('Upload failed'); + } + + const uploadedFile = (await uploadResponse.json()).data; + + // Update expense with receipt + const client = new ObjectQLClient(); + const expense = await client.update('expense', expenseId!, { + receipt: uploadedFile + }); + + onSuccess?.(expense); + } catch (err) { + setError(err instanceof Error ? err.message : 'Upload failed'); + } finally { + setUploading(false); + } + }; + + return ( +
+ + {uploading &&

Uploading...

} + {error &&

{error}

} +
+ ); +}; +``` + +--- + +## Error Codes + +| Code | Description | +|------|-------------| +| `FILE_TOO_LARGE` | File exceeds `max_size` limit | +| `FILE_TOO_SMALL` | File is smaller than `min_size` | +| `FILE_TYPE_NOT_ALLOWED` | File extension not in `accept` list | +| `IMAGE_DIMENSIONS_INVALID` | Image dimensions don't meet requirements | +| `UPLOAD_FAILED` | General upload failure | +| `STORAGE_QUOTA_EXCEEDED` | Storage quota exceeded | +| `FILE_NOT_FOUND` | Requested file doesn't exist | +| `FILE_ACCESS_DENIED` | User doesn't have permission to access file | + +--- + +## TypeScript Definitions + +```typescript +/** + * Attachment field data structure + */ +export interface AttachmentData { + id?: string; + name: string; + url: string; + size: number; + type: string; + original_name?: string; + uploaded_at?: string; + uploaded_by?: string; +} + +/** + * Image-specific attachment data + */ +export interface ImageAttachmentData extends AttachmentData { + width?: number; + height?: number; + thumbnail_url?: string; + variants?: { + small?: string; + medium?: string; + large?: string; + }; +} + +/** + * Upload response + */ +export interface UploadResponse { + data: AttachmentData; +} + +/** + * Batch upload response + */ +export interface BatchUploadResponse { + data: AttachmentData[]; +} +``` + +--- + +## Server Implementation + +### Setting up File Storage + +ObjectQL provides a flexible file storage abstraction that supports multiple backends. + +#### Using Local File Storage + +```typescript +import { createNodeHandler, LocalFileStorage } from '@objectql/server'; +import { ObjectQL } from '@objectql/core'; +import * as http from 'http'; + +const app = new ObjectQL({ /* ... */ }); + +// Configure local file storage +const fileStorage = new LocalFileStorage({ + baseDir: './uploads', // or process.env.OBJECTQL_UPLOAD_DIR + baseUrl: 'http://localhost:3000/api/files' // or process.env.OBJECTQL_BASE_URL +}); + +// Create HTTP handler with file storage +const handler = createNodeHandler(app, { fileStorage }); + +const server = http.createServer(handler); +server.listen(3000); +``` + +#### Using Memory Storage (For Testing) + +```typescript +import { MemoryFileStorage } from '@objectql/server'; + +const fileStorage = new MemoryFileStorage({ + baseUrl: 'http://localhost:3000/api/files' +}); + +const handler = createNodeHandler(app, { fileStorage }); +``` + +#### Custom Storage Implementation + +You can implement custom storage backends by implementing the `IFileStorage` interface: + +```typescript +import { IFileStorage, AttachmentData, FileStorageOptions } from '@objectql/server'; + +class S3FileStorage implements IFileStorage { + async save( + file: Buffer, + filename: string, + mimeType: string, + options?: FileStorageOptions + ): Promise { + // Upload to S3 + const key = `${options?.folder || 'uploads'}/${Date.now()}-${filename}`; + await s3.putObject({ + Bucket: 'my-bucket', + Key: key, + Body: file, + ContentType: mimeType + }).promise(); + + return { + id: key, + name: filename, + url: `https://my-bucket.s3.amazonaws.com/${key}`, + size: file.length, + type: mimeType, + uploaded_at: new Date().toISOString(), + uploaded_by: options?.userId + }; + } + + async get(fileId: string): Promise { + // Download from S3 + const result = await s3.getObject({ + Bucket: 'my-bucket', + Key: fileId + }).promise(); + + return result.Body as Buffer; + } + + async delete(fileId: string): Promise { + // Delete from S3 + await s3.deleteObject({ + Bucket: 'my-bucket', + Key: fileId + }).promise(); + + return true; + } + + getPublicUrl(fileId: string): string { + return `https://my-bucket.s3.amazonaws.com/${fileId}`; + } +} + +// Use custom storage +const fileStorage = new S3FileStorage(); +const handler = createNodeHandler(app, { fileStorage }); +``` + +**For detailed S3 integration guide with complete implementation code, see:** +- [S3 Integration Guide (中文)](../examples/s3-integration-guide-cn.md) - Comprehensive Chinese guide +- [S3 Storage Implementation](../examples/s3-storage-implementation.ts) - Production-ready TypeScript code + +### API Endpoints + +The file upload/download functionality is automatically available when using `createNodeHandler`: + +- **POST /api/files/upload** - Single file upload +- **POST /api/files/upload/batch** - Batch file upload +- **GET /api/files/:fileId** - Download file + +### File Validation + +File validation is automatically enforced based on field configuration: + +```yaml +# expense.object.yml +fields: + receipt: + type: file + label: Receipt + accept: ['.pdf', '.jpg', '.png'] # Only these extensions + max_size: 5242880 # 5MB max + min_size: 1024 # 1KB min +``` + +Validation errors return standardized responses: + +```json +{ + "error": { + "code": "FILE_TOO_LARGE", + "message": "File size (6000000 bytes) exceeds maximum allowed size (5242880 bytes)", + "details": { + "file": "receipt.pdf", + "size": 6000000, + "max_size": 5242880 + } + } +} +``` + +### Environment Variables + +Configure file storage behavior using environment variables: + +| Variable | Description | Default | +|----------|-------------|---------| +| `OBJECTQL_UPLOAD_DIR` | Directory for local file storage | `./uploads` | +| `OBJECTQL_BASE_URL` | Base URL for file access | `http://localhost:3000/api/files` | + +--- + +## Related Documentation + +- [Object Definition Specification](../spec/object.md) - Field type definitions +- [API Reference](./README.md) - Complete API documentation +- [Validation Rules](../spec/validation.md) - File validation configuration +- [Server Integration](../guide/server-integration.md) - Setting up file storage + +--- + +**Last Updated**: January 2026 +**API Version**: 1.0.0 diff --git a/apps/site/content/docs/api/authentication.mdx b/apps/site/content/docs/api/authentication.mdx new file mode 100644 index 00000000..2482f72a --- /dev/null +++ b/apps/site/content/docs/api/authentication.mdx @@ -0,0 +1,84 @@ +--- +title: Authentication & Authorization +--- + +# 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"] +``` diff --git a/apps/site/content/docs/api/client-sdk.mdx b/apps/site/content/docs/api/client-sdk.mdx new file mode 100644 index 00000000..a405a831 --- /dev/null +++ b/apps/site/content/docs/api/client-sdk.mdx @@ -0,0 +1,634 @@ +--- +title: Client SDK Usage Guide +--- + +# Client SDK Usage Guide + +This guide demonstrates how to use the ObjectQL TypeScript client SDK to interact with Data API and Metadata API from frontend applications. + +## Installation + +```bash +npm install @objectql/sdk @objectql/types +``` + +## Overview + +The `@objectql/sdk` package provides two main client classes: + +1. **`DataApiClient`** - For CRUD operations on data records +2. **`MetadataApiClient`** - For reading object schemas and metadata + +All types are defined in `@objectql/types` to maintain zero dependencies and enable frontend usage. + +--- + +## Data API Client + +### Basic Setup + +```typescript +import { DataApiClient } from '@objectql/sdk'; + +const dataClient = new DataApiClient({ + baseUrl: 'http://localhost:3000', + token: 'your-auth-token', // Optional + timeout: 30000 // Optional, defaults to 30s +}); +``` + +### List Records + +```typescript +// Simple list +const response = await dataClient.list('users'); +console.log(response.items); // Array of user records +console.log(response.meta); // Pagination metadata + +// With filters and pagination +const activeUsers = await dataClient.list('users', { + filter: [['status', '=', 'active']], + sort: [['created_at', 'desc']], + limit: 20, + skip: 0, + fields: ['name', 'email', 'status'] +}); + +// TypeScript with generics +interface User { + _id: string; + name: string; + email: string; + status: 'active' | 'inactive'; + created_at: string; +} + +const users = await dataClient.list('users', { + filter: [['status', '=', 'active']] +}); + +users.items?.forEach(user => { + console.log(user.name); // Type-safe! +}); +``` + +### Get Single Record + +```typescript +const user = await dataClient.get('users', 'user_123'); +console.log(user.name); +console.log(user.email); + +// With TypeScript types +const user = await dataClient.get('users', 'user_123'); +``` + +### Create Record + +```typescript +// Create single record +const newUser = await dataClient.create('users', { + name: 'Alice Johnson', + email: 'alice@example.com', + role: 'admin' +}); + +console.log(newUser._id); // Generated ID +console.log(newUser.created_at); // Timestamp + +// With TypeScript +const newUser = await dataClient.create('users', { + name: 'Alice Johnson', + email: 'alice@example.com', + status: 'active' +}); +``` + +### Create Multiple Records + +```typescript +const newUsers = await dataClient.createMany('users', [ + { name: 'Bob', email: 'bob@example.com' }, + { name: 'Charlie', email: 'charlie@example.com' } +]); + +console.log(newUsers.items); // Array of created users +``` + +### Update Record + +```typescript +const updated = await dataClient.update('users', 'user_123', { + status: 'inactive' +}); + +console.log(updated.updated_at); // New timestamp +``` + +### Bulk Update + +```typescript +const result = await dataClient.updateMany('users', { + filters: [['status', '=', 'pending']], + data: { status: 'active' } +}); +``` + +### Delete Record + +```typescript +const result = await dataClient.delete('users', 'user_123'); +console.log(result.success); +``` + +### Bulk Delete + +```typescript +const result = await dataClient.deleteMany('users', { + filters: [['created_at', '<', '2023-01-01']] +}); + +console.log(result.deleted_count); +``` + +### Count Records + +```typescript +const countResult = await dataClient.count('users', [ + ['status', '=', 'active'] +]); + +console.log(countResult.count); +``` + +--- + +## Metadata API Client + +### Basic Setup + +```typescript +import { MetadataApiClient } from '@objectql/sdk'; + +const metadataClient = new MetadataApiClient({ + baseUrl: 'http://localhost:3000', + token: 'your-auth-token' // Optional +}); +``` + +### List All Objects + +```typescript +const objectsResponse = await metadataClient.listObjects(); + +objectsResponse.items.forEach(obj => { + console.log(`${obj.name}: ${obj.label}`); + console.log(` Icon: ${obj.icon}`); + console.log(` Description: ${obj.description}`); +}); +``` + +### Get Object Schema + +```typescript +const userSchema = await metadataClient.getObject('users'); + +console.log(userSchema.name); +console.log(userSchema.label); +console.log(userSchema.description); + +// Iterate through fields +Object.entries(userSchema.fields).forEach(([key, field]) => { + console.log(`${field.name} (${field.type})`); + if (field.required) console.log(' - Required'); + if (field.unique) console.log(' - Unique'); +}); + +// Check available actions +if (userSchema.actions) { + Object.keys(userSchema.actions).forEach(actionName => { + console.log(`Action: ${actionName}`); + }); +} +``` + +### Get Field Metadata + +```typescript +const emailField = await metadataClient.getField('users', 'email'); + +console.log(emailField.type); // "email" +console.log(emailField.required); // true +console.log(emailField.unique); // true +console.log(emailField.label); // "Email Address" +``` + +### List Object Actions + +```typescript +const actionsResponse = await metadataClient.listActions('users'); + +actionsResponse.items.forEach(action => { + console.log(`${action.name} (${action.type})`); + console.log(` Label: ${action.label}`); + console.log(` Description: ${action.description}`); +}); +``` + +### List Custom Metadata + +```typescript +// List all views +const views = await metadataClient.listByType('view'); + +// List all forms +const forms = await metadataClient.listByType('form'); + +// List all pages +const pages = await metadataClient.listByType('page'); +``` + +### Get Specific Metadata + +```typescript +const userListView = await metadataClient.getMetadata('view', 'user_list'); +console.log(userListView); + +const userForm = await metadataClient.getMetadata('form', 'user_create'); +console.log(userForm); +``` + +--- + +## Error Handling + +All API methods throw errors with structured information: + +```typescript +import { ApiErrorCode } from '@objectql/types'; + +try { + const user = await dataClient.get('users', 'invalid_id'); +} catch (error) { + if (error instanceof Error) { + // Error message format: "ERROR_CODE: Error message" + console.error(error.message); + + if (error.message.includes(ApiErrorCode.NOT_FOUND)) { + console.log('User not found'); + } else if (error.message.includes(ApiErrorCode.VALIDATION_ERROR)) { + console.log('Validation failed'); + } else if (error.message.includes(ApiErrorCode.UNAUTHORIZED)) { + console.log('Authentication required'); + } + } +} +``` + +--- + +## React Example + +### Custom Hook for Data Fetching + +```typescript +import { useState, useEffect } from 'react'; +import { DataApiClient } from '@objectql/sdk'; + +const dataClient = new DataApiClient({ + baseUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000' +}); + +export function useObjectData(objectName: string, params?: any) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchData() { + try { + setLoading(true); + const response = await dataClient.list(objectName, params); + setData(response.items || []); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + } + + fetchData(); + }, [objectName, JSON.stringify(params)]); + + return { data, loading, error }; +} +``` + +### Using the Hook + +```typescript +import { useObjectData } from './hooks/useObjectData'; + +interface User { + _id: string; + name: string; + email: string; + status: string; +} + +function UserList() { + const { data: users, loading, error } = useObjectData('users', { + filter: [['status', '=', 'active']], + sort: [['name', 'asc']] + }); + + if (loading) return
Loading...
; + if (error) return
Error: {error.message}
; + + return ( +
    + {users.map(user => ( +
  • + {user.name} - {user.email} +
  • + ))} +
+ ); +} +``` + +### Custom Hook for Metadata + +```typescript +import { useState, useEffect } from 'react'; +import { MetadataApiClient, ObjectMetadataDetail } from '@objectql/sdk'; + +const metadataClient = new MetadataApiClient({ + baseUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000' +}); + +export function useObjectSchema(objectName: string) { + const [schema, setSchema] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchSchema() { + try { + setLoading(true); + const data = await metadataClient.getObject(objectName); + setSchema(data); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + } + + fetchSchema(); + }, [objectName]); + + return { schema, loading, error }; +} +``` + +### Dynamic Form Generator + +```typescript +import { useObjectSchema } from './hooks/useObjectSchema'; + +function DynamicForm({ objectName }: { objectName: string }) { + const { schema, loading, error } = useObjectSchema(objectName); + + if (loading) return
Loading form...
; + if (error) return
Error: {error.message}
; + if (!schema) return null; + + return ( +
+

Create {schema.label}

+ {Object.entries(schema.fields).map(([key, field]) => ( +
+ + {field.type === 'text' && } + {field.type === 'email' && } + {field.type === 'number' && ( + + )} + {field.type === 'select' && ( + + )} +
+ ))} + +
+ ); +} +``` + +--- + +## Vue.js Example + +### Composable for Data Fetching + +```typescript +import { ref, watchEffect } from 'vue'; +import { DataApiClient } from '@objectql/sdk'; + +const dataClient = new DataApiClient({ + baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000' +}); + +export function useObjectData(objectName: string, params?: any) { + const data = ref([]); + const loading = ref(true); + const error = ref(null); + + watchEffect(async () => { + try { + loading.value = true; + const response = await dataClient.list(objectName, params); + data.value = response.items || []; + } catch (err) { + error.value = err as Error; + } finally { + loading.value = false; + } + }); + + return { data, loading, error }; +} +``` + +--- + +## Advanced Filtering + +### Complex Filter Expressions + +```typescript +// AND condition +const result = await dataClient.list('orders', { + filter: [ + 'and', + ['status', '=', 'pending'], + ['total', '>', 100] + ] +}); + +// OR condition +const result = await dataClient.list('users', { + filter: [ + 'or', + ['role', '=', 'admin'], + ['role', '=', 'manager'] + ] +}); + +// Nested conditions +const result = await dataClient.list('orders', { + filter: [ + 'and', + ['status', '=', 'pending'], + [ + 'or', + ['priority', '=', 'high'], + ['total', '>', 1000] + ] + ] +}); +``` + +### Expanding Relations + +```typescript +const orders = await dataClient.list('orders', { + expand: { + customer: { + fields: ['name', 'email'] + }, + items: { + fields: ['product_name', 'quantity', 'price'] + } + } +}); + +orders.items?.forEach(order => { + console.log(order.customer.name); + order.items.forEach(item => { + console.log(` - ${item.product_name} x${item.quantity}`); + }); +}); +``` + +--- + +## Type Safety Benefits + +By using the type definitions from `@objectql/types`, you get: + +1. **Autocomplete** - IDEs provide intelligent suggestions +2. **Type Checking** - Catch errors at compile time +3. **Documentation** - Inline JSDoc comments explain each field +4. **Refactoring** - Safely rename and restructure code + +```typescript +import type { + DataApiListParams, + DataApiListResponse, + DataApiItemResponse, + ApiErrorCode, + MetadataApiObjectDetailResponse +} from '@objectql/types'; + +// These types ensure your frontend code stays in sync with the API +``` + +--- + +## Best Practices + +1. **Centralize Client Instances** + ```typescript + // api-clients.ts + export const dataClient = new DataApiClient({ + baseUrl: process.env.API_URL + }); + + export const metadataClient = new MetadataApiClient({ + baseUrl: process.env.API_URL + }); + ``` + +2. **Use Generic Types** + ```typescript + interface Project { + _id: string; + name: string; + status: string; + } + + const projects = await dataClient.list('projects'); + ``` + +3. **Handle Errors Gracefully** + ```typescript + try { + await dataClient.create('users', userData); + } catch (error) { + // Show user-friendly message + toast.error('Failed to create user'); + } + ``` + +4. **Cache Metadata** + ```typescript + // Metadata rarely changes, so cache it + const schemaCache = new Map(); + + async function getSchema(objectName: string) { + if (!schemaCache.has(objectName)) { + const schema = await metadataClient.getObject(objectName); + schemaCache.set(objectName, schema); + } + return schemaCache.get(objectName); + } + ``` + +5. **Use Environment Variables** + ```typescript + const dataClient = new DataApiClient({ + baseUrl: process.env.NEXT_PUBLIC_API_URL, + token: process.env.NEXT_PUBLIC_API_TOKEN + }); + ``` + +--- + +## Summary + +The ObjectQL client SDK provides: + +- ✅ **Type-safe** API clients for Data and Metadata operations +- ✅ **Zero dependencies** in `@objectql/types` for frontend compatibility +- ✅ **RESTful interface** matching the server implementation +- ✅ **Framework agnostic** - works with React, Vue, Angular, etc. +- ✅ **Full TypeScript support** with generics and inference + +For more information, see: +- [REST API Documentation](./rest.md) +- [Metadata API Documentation](./metadata.md) +- [Error Handling Guide](./error-handling.md) diff --git a/apps/site/content/docs/api/custom-routes.mdx b/apps/site/content/docs/api/custom-routes.mdx new file mode 100644 index 00000000..d2e46b32 --- /dev/null +++ b/apps/site/content/docs/api/custom-routes.mdx @@ -0,0 +1,355 @@ +--- +title: Custom API Routes Configuration +--- + +# Custom API Routes Configuration + +## Overview + +ObjectQL allows you to configure custom API route paths during initialization instead of using hardcoded default paths. This feature provides flexibility for: + +- **API Versioning**: Use paths like `/v1/api`, `/v2/api` +- **Custom Naming**: Use domain-specific naming like `/resources`, `/schema` +- **Multiple API Instances**: Run multiple ObjectQL instances with different paths +- **Integration Requirements**: Align with existing API structures + +## Default Routes + +By default, ObjectQL uses these API paths: + +| Endpoint Type | Default Path | Description | +|--------------|--------------|-------------| +| JSON-RPC | `/api/objectql` | Remote procedure calls | +| REST Data API | `/api/data` | CRUD operations on objects | +| Metadata API | `/api/metadata` | Schema and metadata information | +| File Operations | `/api/files` | File upload and download | + +## Configuration + +### Basic Usage + +Configure custom routes when creating handlers: + +```typescript +import { createNodeHandler, createRESTHandler, createMetadataHandler } from '@objectql/server'; + +// Define custom routes +const customRoutes = { + rpc: '/v1/rpc', + data: '/v1/resources', + metadata: '/v1/schema', + files: '/v1/storage' +}; + +// Create handlers with custom routes +const nodeHandler = createNodeHandler(app, { routes: customRoutes }); +const restHandler = createRESTHandler(app, { routes: customRoutes }); +const metadataHandler = createMetadataHandler(app, { routes: customRoutes }); +``` + +### Route Configuration Interface + +```typescript +interface ApiRouteConfig { + /** + * Base path for JSON-RPC endpoint + * @default '/api/objectql' + */ + rpc?: string; + + /** + * Base path for REST data API + * @default '/api/data' + */ + data?: string; + + /** + * Base path for metadata API + * @default '/api/metadata' + */ + metadata?: string; + + /** + * Base path for file operations + * @default '/api/files' + */ + files?: string; +} +``` + +## Complete Example + +```typescript +import express from 'express'; +import { ObjectQL } from '@objectql/core'; +import { SqlDriver } from '@objectql/driver-sql'; +import { createNodeHandler, createRESTHandler, createMetadataHandler } from '@objectql/server'; + +async function main() { + // 1. Initialize ObjectQL + const app = new ObjectQL({ + datasources: { + default: new SqlDriver({ + client: 'sqlite3', + connection: { filename: ':memory:' }, + useNullAsDefault: true + }) + } + }); + + // Register your objects + app.registerObject({ + name: 'user', + label: 'User', + fields: { + name: { type: 'text', label: 'Name' }, + email: { type: 'email', label: 'Email' } + } + }); + + await app.init(); + + // 2. Define custom API routes + const customRoutes = { + rpc: '/v1/rpc', + data: '/v1/resources', + metadata: '/v1/schema', + files: '/v1/storage' + }; + + // 3. Create handlers with custom routes + const nodeHandler = createNodeHandler(app, { routes: customRoutes }); + const restHandler = createRESTHandler(app, { routes: customRoutes }); + const metadataHandler = createMetadataHandler(app, { routes: customRoutes }); + + // 4. Setup Express with custom paths + const server = express(); + + server.all('/v1/rpc*', nodeHandler); + server.all('/v1/resources/*', restHandler); + server.all('/v1/schema*', metadataHandler); + + server.listen(3000, () => { + console.log('🚀 Server running with custom routes'); + console.log(' JSON-RPC: http://localhost:3000/v1/rpc'); + console.log(' REST API: http://localhost:3000/v1/resources'); + console.log(' Metadata: http://localhost:3000/v1/schema'); + console.log(' Files: http://localhost:3000/v1/storage'); + }); +} + +main().catch(console.error); +``` + +## Using Custom Routes + +### JSON-RPC Endpoint + +**Default:** `POST /api/objectql` +**Custom:** `POST /v1/rpc` + +```bash +curl -X POST http://localhost:3000/v1/rpc \ + -H "Content-Type: application/json" \ + -d '{ + "op": "find", + "object": "user", + "args": {} + }' +``` + +### REST Data API + +**Default:** `/api/data/:object` +**Custom:** `/v1/resources/:object` + +```bash +# List users +curl http://localhost:3000/v1/resources/user + +# Get specific user +curl http://localhost:3000/v1/resources/user/123 + +# Create user +curl -X POST http://localhost:3000/v1/resources/user \ + -H "Content-Type: application/json" \ + -d '{"name": "Alice", "email": "alice@example.com"}' + +# Update user +curl -X PUT http://localhost:3000/v1/resources/user/123 \ + -H "Content-Type: application/json" \ + -d '{"name": "Alice Updated"}' + +# Delete user +curl -X DELETE http://localhost:3000/v1/resources/user/123 +``` + +### Metadata API + +**Default:** `/api/metadata` +**Custom:** `/v1/schema` + +```bash +# List all objects +curl http://localhost:3000/v1/schema/objects + +# Get object details +curl http://localhost:3000/v1/schema/object/user + +# Get field metadata +curl http://localhost:3000/v1/schema/object/user/fields/email + +# List object actions +curl http://localhost:3000/v1/schema/object/user/actions +``` + +### File Operations + +**Default:** `/api/files` +**Custom:** `/v1/storage` + +```bash +# Upload file +curl -X POST http://localhost:3000/v1/storage/upload \ + -F "file=@myfile.pdf" \ + -F "object=document" \ + -F "field=attachment" + +# Download file +curl http://localhost:3000/v1/storage/abc123 +``` + +## Client SDK Configuration + +The ObjectQL SDK clients also support custom route configuration: + +### Data API Client + +```typescript +import { DataApiClient } from '@objectql/sdk'; + +const client = new DataApiClient({ + baseUrl: 'http://localhost:3000', + dataPath: '/v1/resources' // Custom data path +}); + +const users = await client.list('user'); +``` + +### Metadata API Client + +```typescript +import { MetadataApiClient } from '@objectql/sdk'; + +const client = new MetadataApiClient({ + baseUrl: 'http://localhost:3000', + metadataPath: '/v1/schema' // Custom metadata path +}); + +const objects = await client.listObjects(); +``` + +### Remote Driver + +```typescript +import { RemoteDriver } from '@objectql/sdk'; + +const driver = new RemoteDriver( + 'http://localhost:3000', + '/v1/rpc' // Custom RPC path +); +``` + +## Common Use Cases + +### API Versioning + +Support multiple API versions simultaneously: + +```typescript +// API v1 +const v1Routes = { + rpc: '/api/v1/rpc', + data: '/api/v1/data', + metadata: '/api/v1/metadata', + files: '/api/v1/files' +}; + +// API v2 +const v2Routes = { + rpc: '/api/v2/rpc', + data: '/api/v2/data', + metadata: '/api/v2/metadata', + files: '/api/v2/files' +}; + +const v1Handler = createNodeHandler(appV1, { routes: v1Routes }); +const v2Handler = createNodeHandler(appV2, { routes: v2Routes }); + +server.all('/api/v1/*', v1Handler); +server.all('/api/v2/*', v2Handler); +``` + +### Domain-Specific Naming + +Use business-friendly terminology: + +```typescript +const businessRoutes = { + rpc: '/business/operations', + data: '/business/entities', + metadata: '/business/definitions', + files: '/business/documents' +}; +``` + +### Multi-Tenant Applications + +Isolate APIs per tenant: + +```typescript +app.use('/:tenantId/api/*', (req, res, next) => { + const tenantRoutes = { + rpc: `/${req.params.tenantId}/api/rpc`, + data: `/${req.params.tenantId}/api/data`, + metadata: `/${req.params.tenantId}/api/metadata`, + files: `/${req.params.tenantId}/api/files` + }; + + const handler = createNodeHandler( + getTenantApp(req.params.tenantId), + { routes: tenantRoutes } + ); + + handler(req, res); +}); +``` + +## Backward Compatibility + +All handlers maintain backward compatibility: + +- If no `routes` option is provided, default paths are used +- Existing applications continue to work without changes +- Migration to custom routes is opt-in + +```typescript +// This still works with default routes +const handler = createNodeHandler(app); +// Uses /api/objectql, /api/data, /api/metadata, /api/files +``` + +## Best Practices + +1. **Consistency**: Use the same route structure across all handlers +2. **Documentation**: Document your custom routes for API consumers +3. **Versioning**: Consider using versioned paths for production APIs +4. **Testing**: Test custom routes thoroughly before deployment +5. **Migration**: Plan gradual migration if changing existing routes + +## Related Documentation + +- [REST API Reference](./rest.md) +- [JSON-RPC API Reference](./json-rpc.md) +- [Metadata API Reference](./metadata.md) +- [Client SDK Guide](./client-sdk.md) diff --git a/apps/site/content/docs/api/error-handling.mdx b/apps/site/content/docs/api/error-handling.mdx new file mode 100644 index 00000000..3439217c --- /dev/null +++ b/apps/site/content/docs/api/error-handling.mdx @@ -0,0 +1,77 @@ +--- +title: Error Handling +--- + +# 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" + } +} +``` diff --git a/apps/site/content/docs/api/examples.mdx b/apps/site/content/docs/api/examples.mdx new file mode 100644 index 00000000..12eacbd1 --- /dev/null +++ b/apps/site/content/docs/api/examples.mdx @@ -0,0 +1,149 @@ +--- +title: Examples +--- + +# 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 user = await response.json(); +// { id: 'user_123', email: 'alice@example.com', '@type': 'users', ... } + +// 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 { items } = 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' } + ] + } + } + }) +}); +``` diff --git a/apps/site/content/docs/api/graphql.mdx b/apps/site/content/docs/api/graphql.mdx new file mode 100644 index 00000000..087318cb --- /dev/null +++ b/apps/site/content/docs/api/graphql.mdx @@ -0,0 +1,951 @@ +--- +title: GraphQL API +--- + +# GraphQL API + +ObjectQL provides a **GraphQL interface** for flexible, efficient queries with complex multi-table relationships. GraphQL allows clients to request exactly the data they need in a single request, making it ideal for modern frontends with complex data requirements. + +## Overview + +The GraphQL API provides: +- **Strongly-typed schema** automatically generated from ObjectQL metadata +- **Single endpoint** for all queries and mutations +- **Efficient data fetching** with precise field selection +- **Real-time introspection** for developer tools +- **Standards-compliant** GraphQL implementation + +## Endpoint + +``` +POST /api/graphql +GET /api/graphql +``` + +Both GET and POST methods are supported: +- **POST**: Send queries in request body (recommended for most cases) +- **GET**: Send queries via URL parameters (useful for simple queries and caching) + +--- + +## Getting Started + +## Installation + +The GraphQL adapter is included in `@objectql/server`: + +```typescript +import { createGraphQLHandler } from '@objectql/server'; +import { ObjectQL } from '@objectql/core'; + +const app = new ObjectQL({ + datasources: { + default: myDriver + } +}); + +// Create GraphQL handler +const graphqlHandler = createGraphQLHandler(app); + +// Use with your HTTP server +server.on('request', (req, res) => { + if (req.url?.startsWith('/api/graphql')) { + return graphqlHandler(req, res); + } + // ... other handlers +}); +``` + +## Basic Query Example + +```graphql +query { + user(id: "user_123") { + id + name + email + } +} +``` + +**Response:** +```json +{ + "data": { + "user": { + "id": "user_123", + "name": "Alice", + "email": "alice@example.com" + } + } +} +``` + +--- + +## Schema Generation + +The GraphQL schema is **automatically generated** from your ObjectQL metadata. Each object definition creates: + +1. **Output Type**: For query results (e.g., `User`, `Task`) +2. **Input Type**: For mutations (e.g., `UserInput`, `TaskInput`) +3. **Query Fields**: For fetching data (e.g., `user(id)`, `userList()`) +4. **Mutation Fields**: For creating/updating/deleting data + +## Example Object Definition + +```yaml +# user.object.yml +name: user +label: User +fields: + name: + type: text + required: true + email: + type: email + required: true + age: + type: number + role: + type: select + options: [admin, user, guest] +``` + +**Generated GraphQL Types:** + +```graphql +type User { + id: String! + name: String! + email: String! + age: Float + role: String +} + +input UserInput { + name: String + email: String + age: Float + role: String +} +``` + +--- + +## Queries + +## Fetch Single Record + +Query a single record by ID: + +```graphql +query { + user(id: "user_123") { + id + name + email + role + } +} +``` + +## Fetch Multiple Records + +Query multiple records with optional filtering and pagination: + +```graphql +query { + userList(limit: 10, skip: 0) { + id + name + email + } +} +``` + +**Available Arguments:** +- `limit` (Int): Maximum number of records to return +- `skip` (Int): Number of records to skip (for pagination) +- `filters` (String): JSON-encoded filter expression +- `fields` (List): Specific fields to include +- `sort` (String): JSON-encoded sort specification + +## Advanced Filtering + +Use the `filters` argument with JSON-encoded filter expressions: + +```graphql +query { + userList( + filters: "[[\\"role\\", \\"=\\", \\"admin\\"], \\"and\\", [\\"age\\", \\">=\\", 30]]" + limit: 20 + ) { + id + name + role + age + } +} +``` + +## Sorting + +Use the `sort` argument with JSON-encoded sort specification: + +```graphql +query { + userList( + sort: "[[\\"created_at\\", \\"desc\\"]]" + ) { + id + name + created_at + } +} +``` + +## Field Selection + +GraphQL's field selection naturally limits the data returned: + +```graphql +query { + userList { + id + name + # Only these two fields are returned + } +} +``` + +--- + +## Mutations + +## Create Record + +```graphql +mutation { + createUser(input: { + name: "Bob" + email: "bob@example.com" + role: "user" + }) { + id + name + email + } +} +``` + +**Response:** +```json +{ + "data": { + "createUser": { + "id": "user_456", + "name": "Bob", + "email": "bob@example.com" + } + } +} +``` + +## Update Record + +```graphql +mutation { + updateUser(id: "user_123", input: { + name: "Alice Updated" + role: "admin" + }) { + id + name + role + updated_at + } +} +``` + +## Delete Record + +```graphql +mutation { + deleteUser(id: "user_123") { + id + deleted + } +} +``` + +**Response:** +```json +{ + "data": { + "deleteUser": { + "id": "user_123", + "deleted": true + } + } +} +``` + +--- + +## Variables + +GraphQL variables provide a cleaner way to pass dynamic values: + +## Query with Variables + +```graphql +query GetUser($userId: String!) { + user(id: $userId) { + id + name + email + } +} +``` + +**Variables:** +```json +{ + "userId": "user_123" +} +``` + +**Request (POST):** +```json +{ + "query": "query GetUser($userId: String!) { user(id: $userId) { id name email } }", + "variables": { + "userId": "user_123" + } +} +``` + +## Mutation with Variables + +```graphql +mutation CreateUser($input: UserInput!) { + createUser(input: $input) { + id + name + email + } +} +``` + +**Variables:** +```json +{ + "input": { + "name": "Charlie", + "email": "charlie@example.com", + "role": "user" + } +} +``` + +--- + +## GET Requests + +For simple queries, you can use GET requests with URL parameters: + +```bash +GET /api/graphql?query={user(id:"user_123"){id,name,email}} +``` + +With variables: + +```bash +GET /api/graphql?query=query GetUser($id:String!){user(id:$id){name}}&variables={"id":"user_123"} +``` + +**Note:** GET requests are useful for: +- Simple queries that can be cached +- Direct browser testing +- Debugging and development + +For complex queries or mutations, use POST requests. + +--- + +## Error Handling + +## GraphQL Errors + +Errors follow the GraphQL specification: + +```json +{ + "errors": [ + { + "message": "Object 'nonexistent' not found", + "locations": [{"line": 1, "column": 3}], + "path": ["nonexistent"] + } + ], + "data": null +} +``` + +## Validation Errors + +```json +{ + "errors": [ + { + "message": "Validation failed", + "extensions": { + "code": "VALIDATION_ERROR" + } + } + ] +} +``` + +## Not Found + +```json +{ + "data": { + "user": null + } +} +``` + +--- + +## Type Mapping + +ObjectQL field types are mapped to GraphQL types: + +| ObjectQL Type | GraphQL Type | Notes | +|--------------|--------------|-------| +| `text`, `textarea`, `email`, `url`, `phone` | `String` | Text fields | +| `number`, `currency`, `percent` | `Float` | Numeric fields | +| `auto_number` | `Int` | Integer fields | +| `boolean` | `Boolean` | True/false | +| `date`, `datetime`, `time` | `String` | ISO 8601 format | +| `select` | `String` | String enum values | +| `lookup`, `master_detail` | `String` | Reference by ID | +| `file`, `image` | `String` | File metadata as JSON | +| `object`, `json` | `String` | JSON as string | + +## Required Fields + +Fields marked as `required: true` in ObjectQL become non-nullable (`!`) in GraphQL: + +```yaml +# ObjectQL +fields: + name: + type: text + required: true +``` + +```graphql +# GraphQL +type User { + name: String! # Non-nullable +} +``` + +--- + +## Introspection + +GraphQL provides built-in introspection for schema discovery: + +## Get All Types + +```graphql +{ + __schema { + types { + name + kind + description + } + } +} +``` + +## Get Type Details + +```graphql +{ + __type(name: "User") { + name + kind + fields { + name + type { + name + kind + } + } + } +} +``` + +## Available Operations + +```graphql +{ + __schema { + queryType { + fields { + name + description + } + } + mutationType { + fields { + name + description + } + } + } +} +``` + +--- + +## Client Integration + +## JavaScript/TypeScript + +```typescript +const query = ` + query GetUsers { + userList(limit: 10) { + id + name + email + } + } +`; + +const response = await fetch('/api/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ query }) +}); + +const result = await response.json(); +console.log(result.data.userList); +``` + +## Apollo Client + +```typescript +import { ApolloClient, InMemoryCache, gql } from '@apollo/client'; + +const client = new ApolloClient({ + uri: '/api/graphql', + cache: new InMemoryCache() +}); + +const { data } = await client.query({ + query: gql` + query GetUsers { + userList { + id + name + email + } + } + ` +}); +``` + +## React with Apollo + +```tsx +import { useQuery, gql } from '@apollo/client'; + +const GET_USERS = gql` + query GetUsers { + userList { + id + name + email + } + } +`; + +function UserList() { + const { loading, error, data } = useQuery(GET_USERS); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + return ( +
    + {data.userList.map(user => ( +
  • {user.name}
  • + ))} +
+ ); +} +``` + +--- + +## Development Tools + +## GraphQL Playground + +ObjectQL doesn't include GraphQL Playground by default, but you can easily add it: + +```typescript +import express from 'express'; +import { graphqlHTTP } from 'express-graphql'; +import { createGraphQLHandler, generateGraphQLSchema } from '@objectql/server'; + +const app = express(); +const schema = generateGraphQLSchema(objectQLApp); + +app.use('/api/graphql', graphqlHTTP({ + schema, + graphiql: true // Enable GraphiQL interface +})); +``` + +## Postman + +Import the GraphQL schema into Postman for testing: +1. Create a new GraphQL request +2. Point to `/api/graphql` +3. Use the introspection feature to load the schema + +--- + +## Best Practices + +## 1. Request Only What You Need + +❌ **Don't** request all fields: +```graphql +query { + userList { + id + name + email + age + role + created_at + updated_at + # ... many more fields + } +} +``` + +✅ **Do** request specific fields: +```graphql +query { + userList { + id + name + email + } +} +``` + +## 2. Use Variables for Dynamic Values + +❌ **Don't** embed values in queries: +```graphql +query { + user(id: "user_123") { + name + } +} +``` + +✅ **Do** use variables: +```graphql +query GetUser($id: String!) { + user(id: $id) { + name + } +} +``` + +## 3. Use Fragments for Reusability + +```graphql +fragment UserBasic on User { + id + name + email +} + +query { + user(id: "user_123") { + ...UserBasic + role + } + + userList(limit: 10) { + ...UserBasic + } +} +``` + +## 4. Implement Pagination + +```graphql +query GetUsersPaginated($limit: Int!, $skip: Int!) { + userList(limit: $limit, skip: $skip) { + id + name + email + } +} +``` + +## 5. Handle Errors Gracefully + +```typescript +const result = await fetch('/api/graphql', { + method: 'POST', + body: JSON.stringify({ query }) +}); + +const json = await result.json(); + +if (json.errors) { + console.error('GraphQL errors:', json.errors); + // Handle errors appropriately +} + +if (json.data) { + // Process data +} +``` + +--- + +## Comparison with Other APIs + +## GraphQL vs REST + +| Feature | GraphQL | REST | +|---------|---------|------| +| **Endpoint** | Single endpoint | Multiple endpoints | +| **Data Fetching** | Precise field selection | Fixed response structure | +| **Multiple Resources** | Single request | Multiple requests | +| **Over-fetching** | No | Common | +| **Under-fetching** | No | Common | +| **Versioning** | Schema evolution | URL versioning | +| **Caching** | More complex | Simple (HTTP) | + +## GraphQL vs JSON-RPC + +| Feature | GraphQL | JSON-RPC | +|---------|---------|----------| +| **Type System** | Strongly typed | Flexible | +| **Introspection** | Built-in | Not available | +| **Field Selection** | Granular | All or custom | +| **Developer Tools** | Excellent | Limited | +| **Learning Curve** | Moderate | Low | +| **Flexibility** | High | Very High | + +## When to Use GraphQL + +**Use GraphQL when:** +- Building complex UIs with nested data requirements +- Client needs flexibility in data fetching +- You want strong typing and introspection +- Reducing network requests is critical +- Working with modern frontend frameworks (React, Vue, Angular) + +**Use REST when:** +- Simple CRUD operations +- Caching is critical +- Working with legacy systems +- Team is more familiar with REST + +**Use JSON-RPC when:** +- Need maximum flexibility +- Building internal microservices +- Working with AI agents +- Custom operations beyond CRUD + +--- + +## Limitations + +## Current Limitations + +1. **No Subscriptions**: Real-time subscriptions are not yet supported +2. **No Nested Mutations**: Cannot create related records in a single mutation +3. **Basic Relationships**: Relationships are represented as IDs, not nested objects +4. **No Custom Scalars**: Uses built-in GraphQL scalars only +5. **No Directives**: Custom directives not supported + +## Planned Features + +- **Nested Relationships**: Query related objects without separate requests +- **Subscriptions**: Real-time updates via WebSocket +- **Custom Scalars**: Date, DateTime, JSON scalars +- **Relay Connections**: Standardized pagination +- **Field Resolvers**: Custom field resolution logic +- **DataLoader Integration**: Batch and cache database queries + +--- + +## Performance Considerations + +## Query Complexity + +ObjectQL GraphQL doesn't currently limit query complexity. For production: + +1. **Implement Rate Limiting**: Limit requests per user/IP +2. **Set Depth Limits**: Prevent deeply nested queries +3. **Monitor Performance**: Track slow queries +4. **Add Caching**: Use Redis or similar for frequently accessed data + +## Database Optimization + +1. **Add Indexes**: Index fields used in filters and sorts +2. **Use Pagination**: Always limit result sets +3. **Optimize Filters**: Use indexed fields in filter conditions + +--- + +## Security + +## Authentication + +GraphQL uses the same authentication as other ObjectQL APIs: + +```typescript +// With JWT +fetch('/api/graphql', { + headers: { + 'Authorization': 'Bearer ' + token + } +}) +``` + +## Authorization + +ObjectQL's permission system works with GraphQL: +- Object-level permissions +- Field-level permissions +- Record-level permissions + +## Best Practices + +1. **Always Authenticate**: Require authentication for mutations +2. **Validate Input**: ObjectQL validates based on schema +3. **Rate Limit**: Prevent abuse +4. **Sanitize Errors**: Don't expose internal details in production +5. **Use HTTPS**: Always in production + +--- + +## Troubleshooting + +## Common Issues + +**Query Returns Null** + +Check that: +- Object exists in metadata +- ID is correct +- User has permission +- Record exists in database + +**Type Errors** + +Ensure: +- Variable types match schema +- Required fields are provided +- Field names are correct + +**Performance Issues** + +Solutions: +- Limit result sets with pagination +- Request only needed fields +- Add database indexes +- Use caching + +--- + +## Examples + +## Complete CRUD Example + +```graphql +# Create +mutation CreateUser($input: UserInput!) { + createUser(input: $input) { + id + name + email + } +} + +# Read One +query GetUser($id: String!) { + user(id: $id) { + id + name + email + role + } +} + +# Read Many +query ListUsers($limit: Int, $skip: Int) { + userList(limit: $limit, skip: $skip) { + id + name + email + } +} + +# Update +mutation UpdateUser($id: String!, $input: UserInput!) { + updateUser(id: $id, input: $input) { + id + name + email + updated_at + } +} + +# Delete +mutation DeleteUser($id: String!) { + deleteUser(id: $id) { + id + deleted + } +} +``` + +--- + +## Further Reading + +- [GraphQL Specification](https://spec.graphql.org/) +- [ObjectQL Query Language](../spec/query-language.md) +- [REST API Documentation](./README.md#rest-style-api) +- [Authentication Guide](./authentication.md) + +--- + +**Last Updated**: January 2026 +**API Version**: 1.0.0 diff --git a/apps/site/content/docs/api/index.mdx b/apps/site/content/docs/api/index.mdx new file mode 100644 index 00000000..c262cf83 --- /dev/null +++ b/apps/site/content/docs/api/index.mdx @@ -0,0 +1,94 @@ +--- +title: API Documentation +--- + +# API Documentation + +Welcome to the ObjectQL API Reference. + +ObjectQL provides a **unified query protocol** that can be exposed through multiple API styles. All styles share the same underlying metadata, validation rules, and permissions system. + +## 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, designed for LLM generation. +4. **Secure**: Built-in validation, permission checks, SQL injection prevention. +5. **Universal**: Same query works across MongoDB, PostgreSQL, SQLite. + +## Unified ID Field + +ObjectQL uses a **unified `id` field** as the primary key across all database drivers: + +- **Consistent Naming**: Always use `id` in API requests and responses. +- **Database Agnostic**: The driver handles mapping (e.g. to `_id` in Mongo) automatically. +- **String Based**: IDs are always strings to ensure JSON compatibility. + +## API Styles Overview + +| API Style | Use Case | Endpoint Pattern | Docs | +|-----------|----------|------------------|------| +| **JSON-RPC** | Universal client, AI agents, microservices | `POST /api/objectql` | [Read Guide](./json-rpc.md) | +| **REST** | Traditional web apps, mobile apps | `/api/data/:object` | [Read Guide](./rest.md) | +| **GraphQL** | Modern frontends with complex data requirements | `POST /api/graphql` | [Read Guide](./graphql.md) | +| **Metadata** | Admin interfaces, schema discovery | `/api/metadata/*` | [Read Guide](./metadata.md) | + +> **🚀 Want to optimize your queries?** +> Check out the [Query Best Practices Guide](../guide/query-best-practices.md) for performance optimization strategies, detailed comparisons, and recommendations to help you choose the best approach for your use case. + +## Quick Links + +### Core Concepts +- [Custom API Routes](./custom-routes.md) ⭐ **NEW** +- [Authentication & Security](./authentication.md) +- [Error Handling](./error-handling.md) +- [Rate Limiting](./rate-limiting.md) +- [Unified API Response Format](./error-handling.md#response-format) + +### Advanced Features +- [File & Attachments API](./attachments.md) +- [Realtime / WebSocket API](./websocket.md) +- [Examples Collection](./examples.md) + +--- + +## Quick Start + +### Basic Query (JSON-RPC) + +```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 + +```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": { + "name": "Complete documentation", + "priority": "high", + "due_date": "2024-01-20" + } + }' +``` + +### Auto-Generated Specs + +For automated tool ingestion, use the following endpoints: +- **OpenAPI / Swagger**: `/openapi.json` (Used by `/docs` UI) +- **GraphQL Schema**: `/api/graphql/schema` diff --git a/apps/site/content/docs/api/json-rpc.mdx b/apps/site/content/docs/api/json-rpc.mdx new file mode 100644 index 00000000..1dcab654 --- /dev/null +++ b/apps/site/content/docs/api/json-rpc.mdx @@ -0,0 +1,499 @@ +--- +title: JSON-RPC Style API +--- + +# 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' | 'createMany' | 'updateMany' | 'deleteMany'; + + // The target object/table + object: string; + + // Operation-specific arguments + args: any; +} +``` + +## Response Format + +```typescript +interface ObjectQLResponse { + // For list operations (find) + items?: any[]; + + // Pagination metadata (for list operations) + meta?: { + total: number; // Total number of records + page?: number; // Current page number (1-indexed) + size?: number; // Number of items per page + pages?: number; // Total number of pages + has_next?: boolean; // Whether there is a next page + }; + + // For single item operations, the response is the object itself with '@type' field + // Examples: findOne, create, update return { id: '...', name: '...', '@type': 'users' } + '@type'?: string; // Object type identifier + + // Error information + error?: { + code: string; + message: string; + details?: any; + }; + + // Other fields from the actual data object (for single item responses) + [key: string]: any; +} +``` + +## 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 +{ + "items": [ + { + "order_no": "ORD-001", + "amount": 1500, + "status": "paid", + "created_at": "2024-01-15T10:30:00Z", + "customer": { + "name": "Acme Corp", + "email": "contact@acme.com" + } + } + ], + "meta": { + "total": 150, + "page": 1, + "size": 20, + "pages": 8, + "has_next": true + } +} +``` + +## 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 +{ + "id": "user_123", + "name": "Alice", + "email": "alice@example.com", + "@type": "users" +} +``` + +## 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 +{ + "id": "task_456", + "name": "Review PR", + "priority": "high", + "assignee_id": "user_123", + "due_date": "2024-01-20", + "created_at": "2024-01-15T10:30:00Z", + "@type": "tasks" +} +``` + +## 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 +{ + "id": "task_456", + "status": "completed", + "completed_at": "2024-01-16T14:00:00Z", + "@type": "tasks" +} +``` + +## 5. `delete` - Delete Record + +Remove a record by ID. + +**Request:** +```json +{ + "op": "delete", + "object": "tasks", + "args": { + "id": "task_456" + } +} +``` + +**Response:** +```json +{ + "id": "task_456", + "deleted": true, + "@type": "tasks" +} +``` + +## 6. `count` - Count Records + +Get the count of records matching a filter. + +**Request:** +```json +{ + "op": "count", + "object": "orders", + "args": { + "filters": [ + ["status", "=", "pending"] + ] + } +} +``` + +**Response:** +```json +{ + "count": 42, + "@type": "orders" +} +``` + +## 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 +{ + "success": true, + "message": "Order approved successfully", + "order": { + "id": "order_789", + "status": "approved", + "approved_at": "2024-01-15T10:30:00Z" + }, + "@type": "orders" +} +``` + +## Bulk Operations + +ObjectQL supports efficient bulk operations for creating, updating, and deleting multiple records in a single request. + +**Important Notes:** +- **Validation & Hooks**: Bulk operations process each record individually to ensure validation rules and hooks (beforeCreate, afterCreate, etc.) are properly executed, maintaining data integrity +- **Atomicity**: Operations are not atomic by default - if one record fails, others may have already been processed +- **Performance**: While bulk operations are more efficient than separate API calls, they may be slower than driver-level bulk operations due to individual validation/hook execution +- **Use Cases**: Use bulk operations when you need consistent validation and business logic enforcement. For high-performance batch imports where validation is already handled, consider using driver-level operations directly + +## 8. `createMany` - Create Multiple Records + +Insert multiple records in a single operation. + +**Request:** +```json +{ + "op": "createMany", + "object": "tasks", + "args": [ + { + "name": "Task 1", + "priority": "high", + "assignee_id": "user_123" + }, + { + "name": "Task 2", + "priority": "medium", + "assignee_id": "user_456" + }, + { + "name": "Task 3", + "priority": "low", + "assignee_id": "user_789" + } + ] +} +``` + +**Response:** +```json +{ + "items": [ + { + "id": "task_101", + "name": "Task 1", + "priority": "high", + "assignee_id": "user_123", + "created_at": "2024-01-15T10:30:00Z" + }, + { + "id": "task_102", + "name": "Task 2", + "priority": "medium", + "assignee_id": "user_456", + "created_at": "2024-01-15T10:30:00Z" + }, + { + "id": "task_103", + "name": "Task 3", + "priority": "low", + "assignee_id": "user_789", + "created_at": "2024-01-15T10:30:00Z" + } + ], + "count": 3, + "@type": "tasks" +} +``` + +## 9. `updateMany` - Update Multiple Records + +Update all records matching a filter. + +**Request:** +```json +{ + "op": "updateMany", + "object": "tasks", + "args": { + "filters": { + "status": "pending", + "priority": "low" + }, + "data": { + "status": "cancelled", + "cancelled_at": "2024-01-15T10:30:00Z" + } + } +} +``` + +**Response:** +```json +{ + "count": 15, + "@type": "tasks" +} +``` + +## 10. `deleteMany` - Delete Multiple Records + +Delete all records matching a filter. + +**Request:** +```json +{ + "op": "deleteMany", + "object": "tasks", + "args": { + "filters": { + "status": "completed", + "completed_at": ["<", "2023-01-01"] + } + } +} +``` + +**Response:** +```json +{ + "count": 42, + "@type": "tasks" +} +``` + +**Error Handling Example:** +```json +// If a record fails validation during bulk operation +{ + "error": { + "code": "VALIDATION_ERROR", + "message": "Validation failed", + "details": { + "fields": { + "priority": "Invalid priority value" + } + } + } +} +``` + +## 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"]] + } +} +``` diff --git a/apps/site/content/docs/api/metadata.mdx b/apps/site/content/docs/api/metadata.mdx new file mode 100644 index 00000000..32a1f012 --- /dev/null +++ b/apps/site/content/docs/api/metadata.mdx @@ -0,0 +1,171 @@ +--- +title: Metadata API +--- + +# 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" + } + ] +} +``` diff --git a/apps/site/content/docs/api/quick-reference.mdx b/apps/site/content/docs/api/quick-reference.mdx new file mode 100644 index 00000000..40d49783 --- /dev/null +++ b/apps/site/content/docs/api/quick-reference.mdx @@ -0,0 +1,498 @@ +--- +title: API Quick Reference +--- + +# 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": { + "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`. diff --git a/apps/site/content/docs/api/rate-limiting.mdx b/apps/site/content/docs/api/rate-limiting.mdx new file mode 100644 index 00000000..ac37e1ed --- /dev/null +++ b/apps/site/content/docs/api/rate-limiting.mdx @@ -0,0 +1,43 @@ +--- +title: Rate Limiting +--- + +# 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` diff --git a/apps/site/content/docs/api/rest.mdx b/apps/site/content/docs/api/rest.mdx new file mode 100644 index 00000000..e9a4205e --- /dev/null +++ b/apps/site/content/docs/api/rest.mdx @@ -0,0 +1,228 @@ +--- +title: REST-Style API +--- + +# 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 (or create many if body is an array) | +| `POST` | `/api/data/:object/bulk-update` | Update many records | +| `POST` | `/api/data/:object/bulk-delete` | Delete many records | +| `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 +{ + "items": [...], + "meta": { + "total": 150, + "page": 1, + "size": 20, + "pages": 8, + "has_next": true + } +} +``` + +## Get Single Record + +```bash +GET /api/data/users/user_123 +``` + +**Response:** +```json +{ + "id": "user_123", + "name": "Alice", + "email": "alice@example.com", + "@type": "users" +} +``` + +## Create Record + +```bash +POST /api/data/users +Content-Type: application/json + +{ + "name": "Bob", + "email": "bob@example.com", + "role": "admin" +} +``` + +**Response:** +```json +{ + "id": "user_456", + "name": "Bob", + "email": "bob@example.com", + "role": "admin", + "created_at": "2024-01-15T10:30:00Z", + "@type": "users" +} +``` + +## Update Record + +```bash +PUT /api/data/users/user_456 +Content-Type: application/json + +{ + "role": "user" +} +``` + +**Response:** +```json +{ + "id": "user_456", + "role": "user", + "updated_at": "2024-01-15T11:00:00Z", + "@type": "users" +} +``` + +## Delete Record + +```bash +DELETE /api/data/users/user_456 +``` + +**Response:** +```json +{ + "id": "user_456", + "deleted": true, + "@type": "users" +} +``` + +## Bulk Operations (REST) + +## Create Many Records + +Send an array in the POST body to create multiple records at once. + +```bash +POST /api/data/users +Content-Type: application/json + +[ + { + "name": "User1", + "email": "user1@example.com", + "role": "user" + }, + { + "name": "User2", + "email": "user2@example.com", + "role": "user" + }, + { + "name": "User3", + "email": "user3@example.com", + "role": "admin" + } +] +``` + +**Response:** +```json +{ + "items": [ + { + "id": "user_101", + "name": "User1", + "email": "user1@example.com", + "role": "user", + "created_at": "2024-01-15T10:30:00Z" + }, + { + "id": "user_102", + "name": "User2", + "email": "user2@example.com", + "role": "user", + "created_at": "2024-01-15T10:30:00Z" + }, + { + "id": "user_103", + "name": "User3", + "email": "user3@example.com", + "role": "admin", + "created_at": "2024-01-15T10:30:00Z" + } + ], + "count": 3, + "@type": "users" +} +``` + +## Update Many Records + +Update all records matching the provided filters. + +```bash +POST /api/data/users/bulk-update +Content-Type: application/json + +{ + "filters": { + "role": "user", + "status": "inactive" + }, + "data": { + "status": "archived", + "archived_at": "2024-01-15T10:30:00Z" + } +} +``` + +**Response:** +```json +{ + "count": 15, + "@type": "users" +} +``` + +## Delete Many Records + +Delete all records matching the provided filters. + +```bash +POST /api/data/users/bulk-delete +Content-Type: application/json + +{ + "filters": { + "status": "archived", + "archived_at": ["<", "2023-01-01"] + } +} +``` + +**Response:** +```json +{ + "count": 42, + "@type": "users" +} +``` diff --git a/apps/site/content/docs/api/websocket.mdx b/apps/site/content/docs/api/websocket.mdx new file mode 100644 index 00000000..ad323379 --- /dev/null +++ b/apps/site/content/docs/api/websocket.mdx @@ -0,0 +1,38 @@ +--- +title: WebSocket API +--- + +# 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); + } +}; +``` diff --git a/apps/site/content/docs/contributing.mdx b/apps/site/content/docs/contributing.mdx new file mode 100644 index 00000000..8dec1a1a --- /dev/null +++ b/apps/site/content/docs/contributing.mdx @@ -0,0 +1,456 @@ +--- +title: Contributing to ObjectQL +--- + +# Contributing to ObjectQL + +Thank you for your interest in contributing to ObjectQL! This guide will help you get started. + +--- + +## 📋 Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Contribution Types](#contribution-types) +- [Pull Request Process](#pull-request-process) +- [Coding Standards](#coding-standards) +- [Testing Guidelines](#testing-guidelines) +- [Documentation](#documentation) + +--- + +## Code of Conduct + +We are committed to providing a welcoming and inclusive environment. Please be respectful and professional in all interactions. + +### Expected Behavior + +- Use welcoming and inclusive language +- Be respectful of differing viewpoints +- Accept constructive criticism gracefully +- Focus on what's best for the community +- Show empathy towards other community members + +--- + +## Getting Started + +### Prerequisites + +- **Node.js** 18+ LTS +- **pnpm** 8.0+ +- **Git** 2.0+ +- A code editor (we recommend VSCode with the ObjectQL extension) + +### Setup Development Environment + +```bash +# Clone the repository +git clone https://github.com/objectstack-ai/objectql.git +cd objectql + +# Install pnpm if you haven't already +npm install -g pnpm + +# Install dependencies +pnpm install + +# Build all packages +pnpm build + +# Run tests +pnpm test +``` + +### Project Structure + +``` +objectql/ +├── packages/ +│ ├── foundation/ # Core packages +│ │ ├── types/ # Type definitions (zero dependencies) +│ │ ├── core/ # Core engine +│ │ └── platform-node/ # Node.js utilities +│ ├── drivers/ # Database drivers +│ │ ├── sql/ +│ │ ├── mongo/ +│ │ ├── memory/ +│ │ └── ... +│ ├── runtime/ # Runtime packages +│ │ └── server/ # HTTP server +│ └── tools/ # Developer tools +│ ├── cli/ +│ ├── create/ +│ └── vscode-objectql/ +├── docs/ # Documentation +├── examples/ # Example applications +└── scripts/ # Build and utility scripts +``` + +--- + +## Development Workflow + +### 1. Pick an Issue + +- Browse [open issues](https://github.com/objectstack-ai/objectql/issues) +- Look for issues labeled `good first issue` or `help wanted` +- Comment on the issue to let others know you're working on it + +### 2. Create a Branch + +```bash +# Create a feature branch from main +git checkout -b feature/your-feature-name + +# Or for bug fixes +git checkout -b fix/issue-number-description +``` + +### 3. Make Changes + +- Write clean, readable code +- Follow the [coding standards](#coding-standards) +- Add tests for your changes +- Update documentation if needed + +### 4. Test Your Changes + +```bash +# Run tests for a specific package +cd packages/foundation/core +pnpm test + +# Run tests for all packages +pnpm -r test + +# Run linter +pnpm lint + +# Build to check for TypeScript errors +pnpm build +``` + +### 5. Commit Your Changes + +We use [Conventional Commits](https://www.conventionalcommits.org/) format: + +```bash +# Format: (): + +git commit -m "feat(core): add support for virtual columns" +git commit -m "fix(driver-sql): resolve connection pool leak" +git commit -m "docs: update getting started guide" +``` + +**Commit Types:** +- `feat:` New feature +- `fix:` Bug fix +- `docs:` Documentation only +- `style:` Code style (formatting, no logic change) +- `refactor:` Code refactoring +- `test:` Adding tests +- `chore:` Maintenance tasks + +### 6. Push and Create Pull Request + +```bash +git push origin feature/your-feature-name +``` + +Then create a pull request on GitHub. + +--- + +## Contribution Types + +### 🐛 Bug Fixes + +1. Find or create an issue describing the bug +2. Include steps to reproduce +3. Write a failing test that reproduces the bug +4. Fix the bug +5. Ensure the test now passes +6. Submit a pull request + +### ✨ New Features + +1. Open an issue to discuss the feature first (for large changes) +2. Get approval from maintainers +3. Implement the feature +4. Add comprehensive tests +5. Update documentation +6. Submit a pull request + +### 📝 Documentation + +- Fix typos or clarify existing docs +- Add examples and tutorials +- Translate documentation to other languages +- Improve API reference docs + +### 🔌 New Drivers + +See the [Driver Extensibility Guide](./docs/guide/drivers/extensibility.md) for detailed instructions. + +Quick steps: +1. Create a new package in `packages/drivers/` +2. Implement the `Driver` interface from `@objectql/types` +3. Add comprehensive tests (aim for 90%+ coverage) +4. Write documentation +5. Add examples +6. Submit a pull request + +--- + +## Pull Request Process + +### PR Checklist + +Before submitting, ensure: + +- [ ] Code follows coding standards +- [ ] Tests are added/updated and passing +- [ ] Documentation is updated +- [ ] Commit messages follow Conventional Commits +- [ ] No breaking changes (or clearly documented) +- [ ] PR description explains the changes + +### PR Template + +```markdown +## Description +Brief description of changes + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## Related Issue +Fixes #123 + +## Testing +Describe how you tested your changes + +## Checklist +- [ ] Tests pass locally +- [ ] Code follows style guidelines +- [ ] Documentation updated +- [ ] No breaking changes (or documented) +``` + +### Review Process + +1. Automated checks will run (tests, linting) +2. Maintainers will review your code +3. Address any feedback +4. Once approved, a maintainer will merge + +--- + +## Coding Standards + +### TypeScript Style + +```typescript +// ✅ DO: Use strict types +interface UserData { + name: string; + age: number; +} + +// ❌ DON'T: Use 'any' +const data: any = { name: "John" }; + +// ✅ DO: Use readonly for immutable data +interface Config { + readonly apiUrl: string; +} + +// ✅ DO: Use generics for reusable code +function identity(value: T): T { + return value; +} +``` + +### Naming Conventions + +- **Interfaces:** PascalCase, prefix with `I` for interfaces (e.g., `IDriver`) +- **Classes:** PascalCase (e.g., `SqlDriver`) +- **Functions:** camelCase (e.g., `createContext`) +- **Constants:** UPPER_SNAKE_CASE (e.g., `DEFAULT_PORT`) +- **Files:** kebab-case (e.g., `formula-engine.ts`) + +### Error Handling + +```typescript +// ✅ DO: Use ObjectQLError +import { ObjectQLError } from '@objectql/types'; + +throw new ObjectQLError({ + code: 'VALIDATION_FAILED', + message: 'Field "name" is required', + details: { field: 'name' } +}); + +// ❌ DON'T: Use generic Error +throw new Error('Something went wrong'); +``` + +### Comments + +```typescript +// ✅ DO: Add JSDoc for public APIs +/** + * Creates a new ObjectQL context + * @param options - Configuration options + * @returns A new context instance + */ +export function createContext(options: ContextOptions): Context { + // ... +} + +// ✅ DO: Explain complex logic +// Calculate the hash using SHA-256 to ensure uniqueness +const hash = crypto.createHash('sha256').update(data).digest('hex'); + +// ❌ DON'T: State the obvious +// Increment i by 1 +i++; +``` + +--- + +## Testing Guidelines + +### Test Structure + +```typescript +import { describe, it, expect, beforeEach } from '@jest/globals'; + +describe('SqlDriver', () => { + let driver: SqlDriver; + + beforeEach(() => { + driver = new SqlDriver(config); + }); + + describe('find', () => { + it('should return records matching the filter', async () => { + const result = await driver.find('users', { + filters: [['status', '=', 'active']] + }); + + expect(result.length).toBeGreaterThan(0); + expect(result[0].status).toBe('active'); + }); + + it('should return empty array when no records match', async () => { + const result = await driver.find('users', { + filters: [['id', '=', 'non-existent']] + }); + + expect(result).toEqual([]); + }); + }); +}); +``` + +### Test Coverage + +- Aim for **85%+** code coverage +- Test happy paths and edge cases +- Test error conditions +- Use mocks sparingly (prefer real implementations) + +### Running Tests + +```bash +# Run all tests +pnpm test + +# Run tests for specific package +cd packages/foundation/core +pnpm test + +# Run tests in watch mode +pnpm test --watch + +# Run tests with coverage +pnpm test --coverage +``` + +--- + +## Documentation + +### Where to Add Documentation + +- **API Reference:** JSDoc comments in TypeScript code +- **User Guides:** `docs/guide/` +- **Tutorials:** `docs/tutorials/` +- **Specifications:** `docs/spec/` +- **Examples:** `docs/examples/` or `examples/` + +### Documentation Style + +- Use clear, concise language +- Include code examples +- Add diagrams where helpful +- Link to related documentation +- Keep it up to date with code changes + +### Example Documentation + +````markdown +# Feature Name + +Brief description of the feature. + +## Basic Usage + +```typescript +import { feature } from '@objectql/core'; + +const result = feature({ + option1: 'value1', + option2: 'value2' +}); +``` + +## Advanced Usage + +More complex examples... + +## API Reference + +### `feature(options)` + +Description of the function. + +**Parameters:** +- `options` (Object) - Configuration options + - `option1` (string) - Description + - `option2` (string) - Description + +**Returns:** `Result` - Description of return value + +## See Also + +- [Related Feature](./related-feature.md) +- [API Reference](/api/) +```` + +--- + +## Questions? + +- **GitHub Discussions:** Ask questions and discuss ideas +- **Discord:** Join our community for real-time help +- **Issues:** Report bugs or request features + +**Thank you for contributing to ObjectQL! 🎉** diff --git a/apps/site/content/docs/development-plan.mdx b/apps/site/content/docs/development-plan.mdx new file mode 100644 index 00000000..481e4ac4 --- /dev/null +++ b/apps/site/content/docs/development-plan.mdx @@ -0,0 +1,561 @@ +--- +title: ObjectQL Development Plan +--- + +# ObjectQL Development Plan + +**Document Version:** 1.0 +**Last Updated:** January 18, 2026 +**Planning Period:** Q1-Q2 2026 +**Status:** Active + +--- + +## Executive Summary + +This document provides a detailed, actionable development plan for the ObjectQL project covering the next 6 months. The plan focuses on achieving production readiness (v3.1.0) and delivering enterprise features (v3.2.0) while maintaining the project's core principles of type safety, security, and AI-native design. + +### Goals for This Period + +1. **Production Readiness** - Achieve 85%+ test coverage and pass security audit +2. **Enterprise Features** - Complete workflow engine and multi-tenancy support +3. **Documentation** - Comprehensive guides and API references +4. **Community Growth** - Expand contributor base and improve onboarding + +--- + +## Phase 1: Foundation Stabilization (Weeks 1-4) + +**Objective:** Stabilize core packages and improve reliability + +### Week 1: Core Engine Audit + +#### Tasks + +**@objectql/types Package** +- [ ] Review all type definitions for consistency +- [ ] Add JSDoc comments to all exported interfaces +- [ ] Create type utility helpers (Pick, Omit, Required variants) +- [ ] Version lock type exports to prevent breaking changes + +**@objectql/core Package** +- [ ] Profile query compilation performance +- [ ] Identify and fix memory leaks in long-running processes +- [ ] Implement query plan caching +- [ ] Add debug logging framework + +**Testing** +- [ ] Create performance benchmark suite +- [ ] Set up automated performance regression detection +- [ ] Document benchmark results as baseline + +**Deliverables:** +- Performance benchmark baseline document +- Core engine optimization report +- Updated type definitions with JSDoc + +--- + +### Week 2: Driver Reliability + +#### Tasks + +**SQL Driver (@objectql/driver-sql)** +- [ ] Implement connection pool health checks +- [ ] Add prepared statement caching +- [ ] Test transaction isolation levels (READ COMMITTED, SERIALIZABLE) +- [ ] Implement automatic reconnection logic +- [ ] Add query timeout configuration + +**MongoDB Driver (@objectql/driver-mongo)** +- [ ] Optimize aggregation pipeline generation +- [ ] Add index hint support +- [ ] Implement connection pooling best practices +- [ ] Test replica set failover scenarios + +**Memory & LocalStorage Drivers** +- [ ] Add size limit configuration +- [ ] Implement LRU eviction for memory driver +- [ ] Add quota exceeded error handling +- [ ] Performance test with large datasets (10K+ records) + +**Testing** +- [ ] Write integration tests for all drivers (target: 90% coverage) +- [ ] Create driver compatibility test suite (TCK) +- [ ] Test concurrent access scenarios +- [ ] Document driver-specific limitations + +**Deliverables:** +- Driver test coverage report (90%+ target) +- Driver compatibility matrix +- Performance comparison benchmarks + +--- + +### Week 3: Error Handling & Logging + +#### Tasks + +**Error System Redesign** +- [ ] Audit all error codes in @objectql/types +- [ ] Create error hierarchy (ValidationError, DatabaseError, etc.) +- [ ] Add error recovery suggestions in error messages +- [ ] Implement error serialization for API responses + +**Logging Infrastructure** +- [ ] Design structured logging format (JSON) +- [ ] Implement configurable log levels (DEBUG, INFO, WARN, ERROR) +- [ ] Add context injection (request ID, user ID) +- [ ] Create log aggregation guide for production + +**Developer Experience** +- [ ] Add helpful error messages with fix suggestions +- [ ] Create error troubleshooting guide +- [ ] Implement error tracking integration (Sentry compatibility) + +**Testing** +- [ ] Write tests for all error scenarios +- [ ] Test error serialization across API boundaries +- [ ] Verify log output formats + +**Deliverables:** +- Standardized error code reference +- Logging configuration guide +- Error troubleshooting documentation + +--- + +### Week 4: Testing & Quality Gates + +#### Tasks + +**Test Coverage** +- [ ] Increase @objectql/core test coverage to 85%+ +- [ ] Add edge case tests for formula engine +- [ ] Write property-based tests for validator +- [ ] Create mutation testing suite + +**Integration Testing** +- [ ] Set up test environment with real databases +- [ ] Write end-to-end tests for common workflows +- [ ] Test cross-driver data migration scenarios +- [ ] Performance test under load (1000 req/s) + +**CI/CD Improvements** +- [ ] Add automated test coverage reporting +- [ ] Set up nightly builds with full test suite +- [ ] Implement automatic version bumping +- [ ] Add release note generation + +**Code Quality** +- [ ] Run ESLint with strict mode +- [ ] Add prettier for consistent formatting +- [ ] Set up Husky pre-commit hooks +- [ ] Configure Dependabot for security updates + +**Deliverables:** +- Test coverage report (85%+ target) +- CI/CD pipeline documentation +- Code quality metrics dashboard + +--- + +## Phase 2: Enterprise Features (Weeks 5-10) + +**Objective:** Implement production-ready enterprise features + +### Week 5-6: Advanced Security + +#### Tasks + +**Permission Compiler Enhancement** +- [ ] Design permission AST representation +- [ ] Implement role hierarchy resolution +- [ ] Add permission inheritance logic +- [ ] Create permission simulation tool for testing + +**Row-Level Security (RLS)** +- [ ] Design RLS rule injection mechanism +- [ ] Implement RLS for SQL drivers (using subqueries) +- [ ] Add RLS for MongoDB (using $match stages) +- [ ] Create performance benchmarks for RLS queries + +**Field-Level Security** +- [ ] Implement column-level access control +- [ ] Add field masking for sensitive data +- [ ] Create field permission testing utilities + +**Audit System** +- [ ] Design audit log schema +- [ ] Implement automatic audit trail for CRUD operations +- [ ] Add change diff generation +- [ ] Create audit query API + +**Testing** +- [ ] Write security test suite +- [ ] Test permission edge cases +- [ ] Verify no permission bypass vulnerabilities +- [ ] Load test RLS impact on query performance + +**Deliverables:** +- Permission compiler implementation +- Audit system documentation +- Security testing guide + +--- + +### Week 7-8: Workflow Engine + +#### Tasks + +**Core Workflow Engine** +- [ ] Define workflow metadata schema (YAML format) +- [ ] Implement state machine execution engine +- [ ] Add transition validation and guards +- [ ] Support for delayed/scheduled transitions + +**Workflow Features** +- [ ] Approval workflows with multi-level approval +- [ ] Parallel task execution +- [ ] Workflow versioning and migration +- [ ] Workflow instance state persistence + +**Integration** +- [ ] Hook integration (beforeTransition, afterTransition) +- [ ] Email/notification triggers +- [ ] Workflow event webhooks +- [ ] Visual workflow status tracking + +**Developer Tools** +- [ ] Workflow definition validator +- [ ] Workflow testing framework +- [ ] Workflow debugging tools +- [ ] Migration tool for workflow schema changes + +**Testing** +- [ ] Write workflow engine unit tests (90%+ coverage) +- [ ] Create workflow integration tests +- [ ] Test concurrent workflow execution +- [ ] Performance test with complex workflows + +**Deliverables:** +- Workflow engine v1.0 +- Workflow specification documentation +- Example workflow definitions +- Workflow tutorial + +--- + +### Week 9-10: Multi-Tenancy + +#### Tasks + +**Tenant Isolation Strategies** +- [ ] Implement schema-per-tenant support +- [ ] Implement row-level tenant isolation +- [ ] Add tenant context to all queries +- [ ] Create tenant switching utilities + +**Tenant Management** +- [ ] Tenant provisioning API +- [ ] Tenant data isolation validation +- [ ] Cross-tenant data sharing controls +- [ ] Tenant deprovisioning and cleanup + +**Performance** +- [ ] Connection pool per tenant +- [ ] Tenant-level query caching +- [ ] Resource quota enforcement per tenant +- [ ] Tenant usage analytics + +**Developer Experience** +- [ ] Tenant context middleware +- [ ] Multi-tenant testing utilities +- [ ] Migration scripts for multi-tenant setup +- [ ] Monitoring and alerting guide + +**Testing** +- [ ] Test data isolation between tenants +- [ ] Test tenant switching performance +- [ ] Verify no cross-tenant data leakage +- [ ] Load test with 1000+ tenants + +**Deliverables:** +- Multi-tenancy implementation +- Tenant isolation test report +- Multi-tenancy setup guide +- Performance benchmarks + +--- + +## Phase 3: Documentation & Polish (Weeks 11-12) + +**Objective:** Complete documentation and prepare for release + +### Week 11: Documentation Sprint + +#### Tasks + +**API Reference** +- [ ] Auto-generate TypeScript API docs +- [ ] Add code examples to all public APIs +- [ ] Create API versioning guide +- [ ] Document breaking changes + +**User Guides** +- [ ] Update getting started guide +- [ ] Write enterprise deployment guide +- [ ] Create performance tuning guide +- [ ] Add security best practices guide + +**Developer Documentation** +- [ ] Architecture deep-dive document +- [ ] Contributing guidelines update +- [ ] Code review checklist +- [ ] Release process documentation + +**Tutorials** +- [ ] Create video tutorial: "Getting Started in 10 Minutes" +- [ ] Write tutorial: "Building a Multi-Tenant SaaS App" +- [ ] Write tutorial: "Implementing Complex Workflows" +- [ ] Create interactive playground examples + +**Deliverables:** +- Complete API reference +- 5+ new user guides +- 3+ video tutorials +- Updated VitePress documentation site + +--- + +### Week 12: Release Preparation + +#### Tasks + +**Quality Assurance** +- [ ] Complete security audit +- [ ] Fix all critical bugs +- [ ] Verify all tests passing +- [ ] Run performance regression tests + +**Release Engineering** +- [ ] Create v3.1.0 and v3.2.0 release branches +- [ ] Generate changelog from commits +- [ ] Update version numbers across packages +- [ ] Tag releases in git + +**Communication** +- [ ] Write release announcement blog post +- [ ] Update README.md with new features +- [ ] Create migration guide from v3.0.0 +- [ ] Prepare demo for community call + +**Deployment** +- [ ] Publish packages to npm +- [ ] Deploy updated documentation site +- [ ] Update GitHub release page +- [ ] Announce on Discord and Twitter + +**Deliverables:** +- v3.1.0 release (production-ready) +- v3.2.0 release (enterprise features) +- Release announcement +- Migration guide + +--- + +## Resource Allocation + +### Core Team Roles + +**Lead Architect** (40 hrs/week) +- System design and architecture decisions +- Code review and quality oversight +- Technical roadmap planning + +**Backend Engineers** (2 × 40 hrs/week) +- Core engine implementation +- Driver development +- Performance optimization + +**Security Engineer** (20 hrs/week) +- Security audit and testing +- Permission system implementation +- Vulnerability assessment + +**Technical Writer** (20 hrs/week) +- Documentation writing +- Tutorial creation +- Example code development + +**DevOps Engineer** (10 hrs/week) +- CI/CD pipeline maintenance +- Release automation +- Monitoring setup + +### Community Contributors + +We welcome community contributions in the following areas: + +- **Driver Development** - New database adapters +- **Documentation** - Guides, tutorials, translations +- **Testing** - Test coverage improvements +- **Examples** - Sample applications +- **Bug Fixes** - Issue resolution + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. + +--- + +## Success Metrics + +### Code Quality Metrics + +| Metric | Current | Target Q2 2026 | +|--------|---------|----------------| +| Test Coverage | 65% | 85% | +| Documentation Coverage | 60% | 95% | +| Performance (queries/sec) | 500 | 1000 | +| Security Score | N/A | A+ (Snyk) | + +### Feature Completion + +| Feature | Status | Target Completion | +|---------|--------|-------------------| +| Production Readiness | 70% | Week 4 | +| Workflow Engine | 35% | Week 8 | +| Multi-Tenancy | 0% | Week 10 | +| Documentation | 75% | Week 11 | + +### Community Growth + +| Metric | Current | Target Q2 2026 | +|--------|---------|----------------| +| GitHub Stars | TBD | +500 | +| Contributors | TBD | +20 | +| Discord Members | TBD | +200 | +| npm Downloads/Month | TBD | 10K+ | + +--- + +## Risk Management + +### Identified Risks + +**Technical Risks** + +1. **Performance Regression** (Medium) + - *Mitigation:* Automated performance testing in CI + - *Contingency:* Rollback mechanism for releases + +2. **Breaking Changes** (High) + - *Mitigation:* Strict semver adherence and deprecation warnings + - *Contingency:* Migration tools and guides + +3. **Security Vulnerabilities** (High) + - *Mitigation:* Regular security audits and Dependabot + - *Contingency:* Emergency patch process + +**Project Risks** + +1. **Resource Constraints** (Medium) + - *Mitigation:* Prioritize critical features + - *Contingency:* Extend timeline if necessary + +2. **Community Adoption** (Low) + - *Mitigation:* Improve documentation and examples + - *Contingency:* Increase marketing efforts + +3. **Dependencies** (Low) + - *Mitigation:* Minimize external dependencies + - *Contingency:* Fork critical dependencies if needed + +--- + +## Dependencies & Prerequisites + +### External Dependencies + +- Node.js 18+ LTS support +- TypeScript 5.3+ +- Database versions: + - PostgreSQL 12+ + - MySQL 8.0+ + - MongoDB 5.0+ + - SQLite 3.35+ + +### Internal Dependencies + +- All packages must depend only on stable APIs +- Cross-package dependencies must be versioned +- No circular dependencies allowed + +### Infrastructure Requirements + +- GitHub Actions for CI/CD +- npm registry for package publishing +- VitePress hosting for documentation +- Test databases for integration testing + +--- + +## Communication Plan + +### Weekly Updates + +- **Monday:** Sprint planning, task assignment +- **Wednesday:** Mid-week sync, blocker resolution +- **Friday:** Demo day, week review + +### Monthly Reviews + +- Last Friday of each month: Roadmap review +- Community call: Feature demos and Q&A +- Blog post: Progress update + +### Release Communication + +- **2 weeks before:** Beta release announcement +- **1 week before:** Release candidate and final testing +- **Release day:** Official announcement and documentation +- **1 week after:** Post-release retrospective + +--- + +## Next Steps + +**Immediate Actions (This Week)** + +1. [ ] Review and approve this development plan +2. [ ] Set up project tracking (GitHub Projects or Jira) +3. [ ] Assign team members to tasks +4. [ ] Schedule weekly standup meetings +5. [ ] Create Phase 1 Week 1 task board + +**Monitoring & Adjustments** + +- Review progress weekly +- Adjust timeline if blockers arise +- Re-prioritize based on user feedback +- Update plan monthly + +--- + +## Appendix + +### Related Documents + +- [ROADMAP.md](./ROADMAP.md) - Long-term vision and milestones +- [CONTRIBUTING.md](./CONTRIBUTING.md) - Contribution guidelines +- [ARCHITECTURE.md](./docs/guide/architecture/overview.md) - System architecture +- [CHANGELOG.md](./CHANGELOG.md) - Version history + +### References + +- [ObjectQL GitHub](https://github.com/objectstack-ai/objectql) +- [Documentation Site](https://docs.objectql.org) +- [Discord Community](https://discord.gg/objectql) + +--- + +*This development plan is a living document and should be updated as the project progresses. All team members are encouraged to provide feedback and suggestions for improvement.* diff --git a/apps/site/content/docs/examples/README_CN.mdx b/apps/site/content/docs/examples/README_CN.mdx new file mode 100644 index 00000000..ae46b9ae --- /dev/null +++ b/apps/site/content/docs/examples/README_CN.mdx @@ -0,0 +1,373 @@ +--- +title: 附件 API 实现文档 +--- + +# 附件 API 实现文档 + +## 概述 + +本次实现为 ObjectQL 添加了完整的文件上传、下载和管理功能。实现包括: + +1. **文件存储抽象层** - 支持本地文件系统和内存存储,可扩展支持 S3、OSS 等云存储 +2. **多部分表单数据解析** - 原生支持 multipart/form-data 文件上传 +3. **文件验证** - 基于字段配置的文件类型、大小验证 +4. **REST API 端点** - `/api/files/upload`、`/api/files/upload/batch`、`/api/files/:fileId` +5. **完整测试覆盖** - 单元测试和集成测试示例 + +## 快速导航 + +📖 **核心指南** +- [附件字段如何实现多选?](./multiple-file-upload-guide-cn.md) - **多文件上传完整指南** +- [如何将附件与记录关联?](./attachment-association-guide-cn.md) +- [如何使用 AWS S3 存储?](./s3-integration-guide-cn.md) +- [文件上传示例代码](./file-upload-example.md) + +## 架构设计 + +### 1. 类型定义 (`types.ts`) + +```typescript +// 附件元数据 +interface AttachmentData { + id?: string; + name: string; + url: string; + size: number; + type: string; + original_name?: string; + uploaded_at?: string; + uploaded_by?: string; +} + +// 文件存储接口 +interface IFileStorage { + save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise; + get(fileId: string): Promise; + delete(fileId: string): Promise; + getPublicUrl(fileId: string): string; +} +``` + +### 2. 存储实现 (`storage.ts`) + +#### LocalFileStorage - 本地文件系统存储 + +```typescript +const storage = new LocalFileStorage({ + baseDir: './uploads', // 文件存储目录 + baseUrl: 'http://localhost:3000/api/files' // 公开访问 URL +}); +``` + +特性: +- 自动创建存储目录 +- 按对象类型组织文件夹结构 +- 生成唯一文件 ID +- 递归搜索文件 + +#### MemoryFileStorage - 内存存储(测试用) + +```typescript +const storage = new MemoryFileStorage({ + baseUrl: 'http://localhost:3000/api/files' +}); +``` + +特性: +- 轻量级,适合测试 +- 无磁盘 I/O +- 可清空所有文件 + +### 3. 文件处理器 (`file-handler.ts`) + +#### 多部分表单解析 + +```typescript +const { fields, files } = await parseMultipart(req, boundary); +``` + +支持: +- 标准 multipart/form-data 格式 +- 多文件上传 +- 表单字段和文件混合 + +#### 文件验证 + +```typescript +const validation = validateFile(file, fieldConfig); +``` + +验证规则: +- `max_size` - 最大文件大小 +- `min_size` - 最小文件大小 +- `accept` - 允许的文件扩展名(如 `['.pdf', '.jpg']`) + +错误响应: +```json +{ + "error": { + "code": "FILE_TOO_LARGE", + "message": "文件大小超出限制", + "details": { ... } + } +} +``` + +### 4. HTTP 端点 (`adapters/node.ts`) + +#### POST /api/files/upload - 单文件上传 + +请求: +```bash +curl -X POST http://localhost:3000/api/files/upload \ + -F "file=@receipt.pdf" \ + -F "object=expense" \ + -F "field=receipt" +``` + +响应: +```json +{ + "data": { + "id": "abc123...", + "name": "abc123.pdf", + "url": "http://localhost:3000/api/files/uploads/expense/abc123.pdf", + "size": 245760, + "type": "application/pdf", + "original_name": "receipt.pdf", + "uploaded_at": "2024-01-15T10:30:00Z" + } +} +``` + +#### POST /api/files/upload/batch - 批量上传 + +请求: +```bash +curl -X POST http://localhost:3000/api/files/upload/batch \ + -F "files=@image1.jpg" \ + -F "files=@image2.jpg" \ + -F "object=product" \ + -F "field=gallery" +``` + +响应: +```json +{ + "data": [ + { "id": "...", "name": "...", "url": "..." }, + { "id": "...", "name": "...", "url": "..." } + ] +} +``` + +#### GET /api/files/:fileId - 文件下载 + +请求: +```bash +curl http://localhost:3000/api/files/abc123 --output file.pdf +``` + +## 使用示例 + +### 服务器端设置 + +```typescript +import { ObjectQL } from '@objectql/core'; +import { createNodeHandler, LocalFileStorage } from '@objectql/server'; +import * as http from 'http'; + +const app = new ObjectQL({ /* ... */ }); + +// 定义带附件字段的对象 +app.registerObject({ + name: 'expense', + fields: { + receipt: { + type: 'file', + accept: ['.pdf', '.jpg', '.png'], + max_size: 5242880 // 5MB + } + } +}); + +await app.init(); + +// 配置文件存储 +const fileStorage = new LocalFileStorage({ + baseDir: './uploads', + baseUrl: 'http://localhost:3000/api/files' +}); + +// 创建 HTTP 服务器 +const handler = createNodeHandler(app, { fileStorage }); +const server = http.createServer(handler); +server.listen(3000); +``` + +### 客户端上传 + +```typescript +// 上传文件 +const formData = new FormData(); +formData.append('file', file); +formData.append('object', 'expense'); +formData.append('field', 'receipt'); + +const uploadRes = await fetch('/api/files/upload', { + method: 'POST', + body: formData +}); + +const { data: uploadedFile } = await uploadRes.json(); + +// 创建记录 +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + expense_number: 'EXP-001', + amount: 125.50, + receipt: uploadedFile + } + }) +}); +``` + +### React 组件 + +```tsx +function FileUpload() { + const [file, setFile] = useState(null); + + const handleUpload = async () => { + if (!file) return; + + const formData = new FormData(); + formData.append('file', file); + + const res = await fetch('/api/files/upload', { + method: 'POST', + body: formData + }); + + const { data } = await res.json(); + console.log('上传成功:', data); + }; + + return ( +
+ setFile(e.target.files?.[0] || null)} + /> + +
+ ); +} +``` + +## 测试 + +### 运行单元测试 + +```bash +cd packages/runtime/server +pnpm test +``` + +测试覆盖: +- ✅ 文件存储(保存、获取、删除) +- ✅ 文件验证(大小、类型) +- ✅ 多部分表单解析 +- ✅ 集成测试示例 + +### 集成测试示例 + +```bash +pnpm test file-upload-integration.example.ts +``` + +## 环境变量 + +| 变量 | 说明 | 默认值 | +|------|------|--------| +| `OBJECTQL_UPLOAD_DIR` | 文件存储目录 | `./uploads` | +| `OBJECTQL_BASE_URL` | 文件访问基础 URL | `http://localhost:3000/api/files` | + +## 扩展性 + +### 自定义存储后端 + +```typescript +import { IFileStorage } from '@objectql/server'; + +class CustomStorage implements IFileStorage { + async save(file: Buffer, filename: string, mimeType: string) { + // 实现自定义存储逻辑(如上传到 S3/OSS) + // ... + return attachmentData; + } + + async get(fileId: string): Promise { + // 实现文件获取逻辑 + // ... + } + + async delete(fileId: string): Promise { + // 实现文件删除逻辑 + // ... + } + + getPublicUrl(fileId: string): string { + // 生成公开访问 URL + return `https://cdn.example.com/${fileId}`; + } +} + +// 使用自定义存储 +const storage = new CustomStorage(); +const handler = createNodeHandler(app, { fileStorage: storage }); +``` + +## 文档更新 + +1. **API 文档** (`docs/api/attachments.md`) + - 添加服务器实现章节 + - 环境变量配置说明 + - 自定义存储示例 + +2. **使用示例** (`docs/examples/file-upload-example.md`) + - 完整的服务器设置代码 + - cURL 命令示例 + - JavaScript/TypeScript 客户端代码 + - React 组件示例 + +## 文件清单 + +### 新增文件 +- `packages/runtime/server/src/storage.ts` - 存储实现 +- `packages/runtime/server/src/file-handler.ts` - 文件处理器 +- `packages/runtime/server/test/storage.test.ts` - 存储测试 +- `packages/runtime/server/test/file-validation.test.ts` - 验证测试 +- `packages/runtime/server/test/file-upload-integration.example.ts` - 集成示例 +- `docs/examples/file-upload-example.md` - 使用示例 +- `docs/examples/README_CN.md` - 本文档 + +### 修改文件 +- `packages/runtime/server/src/types.ts` - 添加附件相关类型 +- `packages/runtime/server/src/adapters/node.ts` - 添加文件端点 +- `packages/runtime/server/src/index.ts` - 导出新模块 +- `docs/api/attachments.md` - 更新实现文档 + +## 后续计划 + +- [ ] 实现缩略图生成端点 `/api/files/:fileId/thumbnail` +- [ ] 添加图片预览端点 `/api/files/:fileId/preview` +- [ ] 支持图片尺寸调整 +- [ ] 集成第三方云存储(S3、OSS、COS) +- [ ] 添加文件访问权限控制 +- [ ] 支持签名 URL(临时访问链接) diff --git a/apps/site/content/docs/examples/attachment-association-guide-cn.mdx b/apps/site/content/docs/examples/attachment-association-guide-cn.mdx new file mode 100644 index 00000000..a99687f7 --- /dev/null +++ b/apps/site/content/docs/examples/attachment-association-guide-cn.mdx @@ -0,0 +1,750 @@ +--- +title: 附件如何与对象记录关联 +--- + +# 附件如何与对象记录关联 + +本文档详细说明在 ObjectQL 中如何将文件附件与对象记录关联,包括两种主要方案和最佳实践。 + +## 目录 + +1. [方案一:嵌入式附件(推荐)](#方案一嵌入式附件推荐) +2. [方案二:独立附件对象](#方案二独立附件对象) +3. [方案对比](#方案对比) +4. [实际应用示例](#实际应用示例) +5. [查询与检索](#查询与检索) +6. [最佳实践](#最佳实践) + +--- + +## 方案一:嵌入式附件(推荐) + +### 设计思路 + +将附件元数据直接存储在对象的字段中,作为 JSON 格式保存在数据库。 + +### 对象定义 + +```yaml +# expense.object.yml +name: expense +label: 报销单 +fields: + expense_number: + type: text + required: true + label: 报销单号 + + amount: + type: number + required: true + label: 金额 + + # 单个附件字段 + receipt: + type: file + label: 收据 + accept: ['.pdf', '.jpg', '.png'] + max_size: 5242880 # 5MB + + # 多个附件字段 + supporting_docs: + type: file + label: 支持文件 + multiple: true + accept: ['.pdf', '.docx', '.xlsx'] +``` + +### 数据结构 + +数据库中存储的格式: + +```json +{ + "id": "exp_001", + "expense_number": "EXP-2024-001", + "amount": 125.50, + "receipt": { + "id": "abc123", + "name": "receipt.pdf", + "url": "https://cdn.example.com/files/receipt.pdf", + "size": 245760, + "type": "application/pdf", + "original_name": "收据.pdf", + "uploaded_at": "2024-01-15T10:30:00Z" + }, + "supporting_docs": [ + { + "id": "def456", + "name": "invoice.pdf", + "url": "https://cdn.example.com/files/invoice.pdf", + "size": 123456, + "type": "application/pdf" + }, + { + "id": "ghi789", + "name": "contract.pdf", + "url": "https://cdn.example.com/files/contract.pdf", + "size": 234567, + "type": "application/pdf" + } + ] +} +``` + +### 完整操作流程 + +#### 1. 上传文件 + +```javascript +// 步骤 1:上传文件到服务器 +const formData = new FormData(); +formData.append('file', fileInput.files[0]); +formData.append('object', 'expense'); // 关联的对象名 +formData.append('field', 'receipt'); // 关联的字段名 + +const uploadRes = await fetch('/api/files/upload', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token + }, + body: formData +}); + +const uploadedFile = (await uploadRes.json()).data; +// { +// id: "abc123", +// name: "abc123.pdf", +// url: "https://cdn.example.com/files/uploads/expense/abc123.pdf", +// size: 245760, +// type: "application/pdf", +// original_name: "收据.pdf", +// uploaded_at: "2024-01-15T10:30:00Z" +// } +``` + +#### 2. 创建记录(附带附件) + +```javascript +// 步骤 2:创建报销单记录,将文件元数据存入 +const createRes = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + expense_number: 'EXP-2024-001', + amount: 125.50, + description: '办公用品采购', + receipt: uploadedFile // 直接传入文件元数据 + } + }) +}); + +const expense = (await createRes.json()); +``` + +#### 3. 更新附件 + +```javascript +// 更新单个附件:上传新文件后替换 +const newFile = await uploadFile(newFileInput.files[0]); + +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'update', + object: 'expense', + args: { + id: 'exp_001', + data: { + receipt: newFile // 替换整个附件 + } + } + }) +}); +``` + +#### 4. 添加多个附件 + +```javascript +// 获取当前记录 +const current = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'findOne', + object: 'expense', + args: 'exp_001' + }) +}).then(r => r.json()); + +// 上传新文件 +const newDoc = await uploadFile(fileInput.files[0]); + +// 追加到数组 +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'update', + object: 'expense', + args: { + id: 'exp_001', + data: { + supporting_docs: [ + ...(current.supporting_docs || []), + newDoc + ] + } + } + }) +}); +``` + +### 优点 + +✅ **简单直观** - 附件与记录一起存储,查询方便 +✅ **性能好** - 无需额外的 JOIN 操作 +✅ **数据完整性** - 附件随记录一起删除 +✅ **适合大多数场景** - 满足 90% 的业务需求 + +### 缺点 + +❌ 不适合需要共享附件的场景 +❌ 无法独立查询所有附件 + +--- + +## 方案二:独立附件对象 + +### 设计思路 + +创建独立的 `attachment` 对象,通过 `related_to` 和 `related_id` 字段关联到其他对象的记录。 + +### 对象定义 + +```yaml +# attachment.object.yml +name: attachment +label: 附件 +fields: + name: + type: text + required: true + label: 文件名 + index: true + + file_url: + type: file + required: true + label: 文件URL + + file_size: + type: number + label: 文件大小(字节) + + file_type: + type: text + label: MIME类型 + index: true + + # 关联字段 + related_to: + type: text + label: 关联对象名 + index: true + + related_id: + type: text + label: 关联记录ID + index: true + + uploaded_by: + type: lookup + reference_to: user + label: 上传者 + + description: + type: textarea + label: 描述 + +indexes: + # 复合索引:快速查询某个记录的所有附件 + related_composite_idx: + fields: [related_to, related_id] +``` + +### 完整操作流程 + +#### 1. 上传文件并创建附件记录 + +```javascript +// 步骤 1:上传文件 +const formData = new FormData(); +formData.append('file', fileInput.files[0]); +formData.append('object', 'attachment'); +formData.append('field', 'file_url'); + +const uploadRes = await fetch('/api/files/upload', { + method: 'POST', + body: formData +}); + +const uploadedFile = (await uploadRes.json()).data; + +// 步骤 2:创建附件记录 +const attachmentRes = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'attachment', + args: { + name: uploadedFile.original_name, + file_url: uploadedFile, + file_size: uploadedFile.size, + file_type: uploadedFile.type, + related_to: 'expense', // 关联的对象名 + related_id: 'exp_001', // 关联的记录ID + description: '报销收据' + } + }) +}); + +const attachment = (await attachmentRes.json()); +``` + +#### 2. 查询某个记录的所有附件 + +```javascript +// 查询报销单 exp_001 的所有附件 +const attachments = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'find', + object: 'attachment', + args: { + filters: [ + ['related_to', '=', 'expense'], + ['related_id', '=', 'exp_001'] + ], + sort: 'created_at desc' + } + }) +}).then(r => r.json()); + +console.log('找到附件:', attachments.items.length); +``` + +#### 3. 删除附件 + +```javascript +// 删除单个附件 +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'delete', + object: 'attachment', + args: { id: 'att_123' } + }) +}); +``` + +#### 4. 批量上传 + +```javascript +async function uploadMultipleAttachments(files, relatedTo, relatedId) { + const attachments = []; + + for (const file of files) { + // 上传文件 + const formData = new FormData(); + formData.append('file', file); + const uploadRes = await fetch('/api/files/upload', { + method: 'POST', + body: formData + }); + const uploadedFile = (await uploadRes.json()).data; + + // 创建附件记录 + const attachmentRes = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'attachment', + args: { + name: uploadedFile.original_name, + file_url: uploadedFile, + file_size: uploadedFile.size, + file_type: uploadedFile.type, + related_to: relatedTo, + related_id: relatedId + } + }) + }); + + attachments.push((await attachmentRes.json())); + } + + return attachments; +} + +// 使用示例 +await uploadMultipleAttachments( + fileInput.files, + 'expense', + 'exp_001' +); +``` + +### 优点 + +✅ **灵活性高** - 附件可以被多个记录共享 +✅ **独立管理** - 可以单独查询、统计所有附件 +✅ **扩展性好** - 易于添加附件相关的功能(如标签、分类) +✅ **适合复杂场景** - 如文档管理系统、知识库 + +### 缺点 + +❌ 查询复杂 - 需要额外的查询来获取附件 +❌ 性能开销 - 需要 JOIN 或多次查询 +❌ 数据一致性 - 删除主记录时需要手动清理附件 + +--- + +## 方案对比 + +| 特性 | 嵌入式附件 | 独立附件对象 | +|------|-----------|-------------| +| **实现难度** | ⭐ 简单 | ⭐⭐⭐ 较复杂 | +| **查询性能** | ⭐⭐⭐ 快 | ⭐⭐ 中等 | +| **灵活性** | ⭐⭐ 中等 | ⭐⭐⭐ 高 | +| **附件共享** | ❌ 不支持 | ✅ 支持 | +| **数据一致性** | ✅ 自动 | ⚠️ 需手动维护 | +| **适用场景** | 简单附件需求 | 复杂文档管理 | + +### 选择建议 + +**使用嵌入式附件(方案一)如果:** +- 附件数量少(1-10 个) +- 附件与记录一对一或一对多 +- 不需要共享附件 +- 追求简单和性能 + +**使用独立附件对象(方案二)如果:** +- 需要独立管理附件 +- 附件需要被多个记录引用 +- 需要复杂的附件查询和统计 +- 构建文档管理系统或知识库 + +--- + +## 实际应用示例 + +### 示例 1:报销系统(嵌入式) + +```yaml +# expense.object.yml +name: expense +fields: + expense_number: + type: text + required: true + amount: + type: number + required: true + receipt: + type: file + label: 收据 + accept: ['.pdf', '.jpg', '.png'] + max_size: 5242880 +``` + +```javascript +// 使用 +const expense = await createExpense({ + expense_number: 'EXP-001', + amount: 125.50, + receipt: uploadedFile +}); +``` + +### 示例 2:客户管理系统(独立附件) + +```yaml +# account.object.yml +name: account +fields: + name: + type: text + required: true + industry: + type: select + options: ['IT', '制造', '金融'] + +# attachment.object.yml +name: attachment +fields: + related_to: + type: text + index: true + related_id: + type: text + index: true + file_url: + type: file +``` + +```javascript +// 上传客户合同 +await createAttachment({ + related_to: 'account', + related_id: 'acc_001', + file_url: contractFile, + name: '服务合同', + category: 'contract' +}); + +// 查询客户的所有文件 +const files = await findAttachments({ + filters: [ + ['related_to', '=', 'account'], + ['related_id', '=', 'acc_001'] + ] +}); +``` + +### 示例 3:产品图库(嵌入式多图) + +```yaml +# product.object.yml +name: product +fields: + name: + type: text + required: true + gallery: + type: image + label: 产品图库 + multiple: true + max_size: 2097152 # 2MB per image +``` + +```javascript +// 批量上传产品图片 +const images = await Promise.all( + Array.from(fileInput.files).map(file => uploadFile(file)) +); + +const product = await createProduct({ + name: '高端笔记本电脑', + gallery: images +}); +``` + +--- + +## 查询与检索 + +### 嵌入式附件查询 + +```javascript +// 查询有附件的记录 +const expensesWithReceipt = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'find', + object: 'expense', + args: { + filters: [['receipt', '!=', null]] + } + }) +}).then(r => r.json()); + +// 查询没有附件的记录 +const expensesWithoutReceipt = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'find', + object: 'expense', + args: { + filters: [['receipt', '=', null]] + } + }) +}).then(r => r.json()); +``` + +### 独立附件对象查询 + +```javascript +// 查询某个对象类型的所有附件 +const expenseAttachments = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'find', + object: 'attachment', + args: { + filters: [['related_to', '=', 'expense']], + sort: 'created_at desc' + } + }) +}).then(r => r.json()); + +// 统计附件数量 +const count = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'count', + object: 'attachment', + args: { + filters: [ + ['related_to', '=', 'expense'], + ['related_id', '=', 'exp_001'] + ] + } + }) +}).then(r => r.json()); + +console.log('附件数量:', count.count); +``` + +--- + +## 最佳实践 + +### 1. 选择合适的方案 + +```javascript +// ✅ 推荐:简单场景使用嵌入式 +const expense = { + expense_number: 'EXP-001', + receipt: uploadedFile // 直接嵌入 +}; + +// ✅ 推荐:复杂场景使用独立对象 +const attachment = { + related_to: 'expense', + related_id: 'exp_001', + file_url: uploadedFile +}; +``` + +### 2. 文件上传验证 + +```javascript +// 客户端验证 +function validateFile(file, fieldConfig) { + // 检查文件类型 + const ext = file.name.substring(file.name.lastIndexOf('.')); + if (!fieldConfig.accept.includes(ext)) { + throw new Error(`不支持的文件类型:${ext}`); + } + + // 检查文件大小 + if (file.size > fieldConfig.max_size) { + throw new Error(`文件过大:${file.size} 字节`); + } +} +``` + +### 3. 错误处理 + +```javascript +async function uploadAndCreateExpense(expenseData, file) { + try { + // 上传文件 + const uploadedFile = await uploadFile(file); + + // 创建记录 + const expense = await createExpense({ + ...expenseData, + receipt: uploadedFile + }); + + return expense; + } catch (error) { + // 如果创建失败,应该清理已上传的文件 + if (uploadedFile) { + await deleteFile(uploadedFile.id); + } + throw error; + } +} +``` + +### 4. 数据清理 + +```javascript +// 删除记录时清理附件(独立附件对象) +async function deleteExpenseWithAttachments(expenseId) { + // 查询关联的附件 + const attachments = await findAttachments({ + filters: [ + ['related_to', '=', 'expense'], + ['related_id', '=', expenseId] + ] + }); + + // 删除附件记录 + for (const attachment of attachments.items) { + await deleteAttachment(attachment.id); + } + + // 删除主记录 + await deleteExpense(expenseId); +} +``` + +### 5. 性能优化 + +```javascript +// ✅ 批量上传优化 +async function batchUpload(files) { + // 并行上传 + const uploads = files.map(file => uploadFile(file)); + return await Promise.all(uploads); +} + +// ✅ 分页查询附件 +const attachments = await findAttachments({ + filters: [['related_to', '=', 'expense']], + limit: 20, + skip: 0, + sort: 'created_at desc' +}); +``` + +--- + +## 总结 + +ObjectQL 提供了两种灵活的附件关联方案: + +1. **嵌入式附件**(推荐用于大多数场景) + - 将附件元数据存储在字段中 + - 简单、高效、易于维护 + +2. **独立附件对象**(用于复杂场景) + - 创建专门的 attachment 对象 + - 通过 related_to/related_id 关联 + - 适合文档管理系统 + +选择合适的方案取决于具体的业务需求。对于简单的附件需求,嵌入式方案足够;对于需要复杂附件管理的系统,使用独立附件对象更合适。 + +**相关文档:** +- [附件 API 文档](../api/attachments.md) +- [文件上传示例](./file-upload-example.md) +- [S3 集成指南](./s3-integration-guide-cn.md) diff --git a/apps/site/content/docs/examples/file-upload-example.mdx b/apps/site/content/docs/examples/file-upload-example.mdx new file mode 100644 index 00000000..fe345985 --- /dev/null +++ b/apps/site/content/docs/examples/file-upload-example.mdx @@ -0,0 +1,287 @@ +--- +title: File Upload Example +--- + +# File Upload Example + +This example demonstrates how to use the ObjectQL file upload API. + +## Setup + +1. Install dependencies: +```bash +pnpm install +``` + +2. Run the example: +```bash +ts-node file-upload-example.ts +``` + +## Code + +```typescript +import { ObjectQL } from '@objectql/core'; +import { SqlDriver } from '@objectql/driver-sql'; +import { createNodeHandler, LocalFileStorage } from '@objectql/server'; +import * as http from 'http'; +import * as fs from 'fs'; + +async function main() { + // 1. Initialize ObjectQL + const driver = new SqlDriver({ + client: 'sqlite3', + connection: { filename: './data.db' }, + useNullAsDefault: true + }); + + const app = new ObjectQL({ + datasources: { default: driver } + }); + + // 2. Define expense object with file attachment + app.registerObject({ + name: 'expense', + label: 'Expense', + fields: { + expense_number: { + type: 'text', + required: true, + label: 'Expense Number' + }, + amount: { + type: 'number', + required: true, + label: 'Amount' + }, + receipt: { + type: 'file', + label: 'Receipt', + accept: ['.pdf', '.jpg', '.png'], + max_size: 5242880 // 5MB + } + } + }); + + await app.init(); + + // 3. Setup file storage + const fileStorage = new LocalFileStorage({ + baseDir: './uploads', + baseUrl: 'http://localhost:3000/api/files' + }); + + // 4. Create HTTP server + const handler = createNodeHandler(app, { fileStorage }); + const server = http.createServer(handler); + + server.listen(3000, () => { + console.log('Server running on http://localhost:3000'); + console.log('\nEndpoints:'); + console.log(' POST /api/files/upload - Upload a file'); + console.log(' POST /api/files/upload/batch - Upload multiple files'); + console.log(' GET /api/files/:fileId - Download a file'); + console.log(' POST /api/objectql - Execute ObjectQL operations'); + console.log(' GET /api/data/expense - List expenses'); + console.log(' GET /api/data/expense/:id - Get expense by ID'); + console.log('\nExample upload:'); + console.log(' curl -X POST http://localhost:3000/api/files/upload \\'); + console.log(' -F "file=@receipt.pdf" \\'); + console.log(' -F "object=expense" \\'); + console.log(' -F "field=receipt"'); + }); +} + +main().catch(console.error); +``` + +## Usage Examples + +### 1. Upload a file + +```bash +curl -X POST http://localhost:3000/api/files/upload \ + -F "file=@receipt.pdf" \ + -F "object=expense" \ + -F "field=receipt" +``` + +Response: +```json +{ + "data": { + "id": "a1b2c3d4e5f6...", + "name": "a1b2c3d4e5f6.pdf", + "url": "http://localhost:3000/api/files/uploads/expense/a1b2c3d4e5f6.pdf", + "size": 245760, + "type": "application/pdf", + "original_name": "receipt.pdf", + "uploaded_at": "2024-01-15T10:30:00Z" + } +} +``` + +### 2. Create expense with uploaded file + +```bash +curl -X POST http://localhost:3000/api/objectql \ + -H "Content-Type: application/json" \ + -d '{ + "op": "create", + "object": "expense", + "args": { + "expense_number": "EXP-2024-001", + "amount": 125.50, + "receipt": { + "id": "a1b2c3d4e5f6...", + "name": "a1b2c3d4e5f6.pdf", + "url": "http://localhost:3000/api/files/uploads/expense/a1b2c3d4e5f6.pdf", + "size": 245760, + "type": "application/pdf", + "original_name": "receipt.pdf", + "uploaded_at": "2024-01-15T10:30:00Z" + } + } + }' +``` + +### 3. Download a file + +```bash +curl http://localhost:3000/api/files/a1b2c3d4e5f6 \ + --output receipt.pdf +``` + +### 4. Query expenses with attachments + +```bash +curl "http://localhost:3000/api/data/expense?filter=\[\[\"receipt\",\"!=\",null\]\]" +``` + +### 5. Upload multiple files (batch) + +```bash +curl -X POST http://localhost:3000/api/files/upload/batch \ + -F "files=@image1.jpg" \ + -F "files=@image2.jpg" \ + -F "files=@image3.jpg" \ + -F "object=product" \ + -F "field=gallery" +``` + +## JavaScript/TypeScript Usage + +```typescript +import { ObjectQLClient } from '@objectql/sdk'; + +const client = new ObjectQLClient({ + baseUrl: 'http://localhost:3000' +}); + +// Upload a file +async function uploadFile(file: File) { + const formData = new FormData(); + formData.append('file', file); + formData.append('object', 'expense'); + formData.append('field', 'receipt'); + + const response = await fetch('http://localhost:3000/api/files/upload', { + method: 'POST', + body: formData + }); + + const { data: uploadedFile } = await response.json(); + return uploadedFile; +} + +// Create expense with uploaded file +async function createExpense(fileData: any) { + const expense = await client.create('expense', { + expense_number: 'EXP-2024-001', + amount: 125.50, + receipt: fileData + }); + + return expense; +} + +// Complete workflow +async function submitExpense(file: File) { + // Step 1: Upload file + const uploadedFile = await uploadFile(file); + + // Step 2: Create expense record + const expense = await createExpense(uploadedFile); + + console.log('Expense created:', expense); +} +``` + +## React Component Example + +```tsx +import React, { useState } from 'react'; + +export function ExpenseForm() { + const [file, setFile] = useState(null); + const [uploading, setUploading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!file) return; + + setUploading(true); + + try { + // Upload file + const formData = new FormData(); + formData.append('file', file); + formData.append('object', 'expense'); + formData.append('field', 'receipt'); + + const uploadResponse = await fetch('/api/files/upload', { + method: 'POST', + body: formData + }); + + const { data: uploadedFile } = await uploadResponse.json(); + + // Create expense + const createResponse = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + expense_number: 'EXP-2024-001', + amount: 125.50, + receipt: uploadedFile + } + }) + }); + + const expense = await createResponse.json(); + alert('Expense created successfully!'); + } catch (error) { + alert('Error: ' + error.message); + } finally { + setUploading(false); + } + }; + + return ( +
+ setFile(e.target.files?.[0] || null)} + /> + +
+ ); +} +``` diff --git a/apps/site/content/docs/examples/multiple-file-upload-guide-cn.mdx b/apps/site/content/docs/examples/multiple-file-upload-guide-cn.mdx new file mode 100644 index 00000000..b8f1da4c --- /dev/null +++ b/apps/site/content/docs/examples/multiple-file-upload-guide-cn.mdx @@ -0,0 +1,564 @@ +--- +title: 附件字段多选功能说明 +--- + +# 附件字段多选功能说明 + +## 概述 + +ObjectQL 附件字段**完全支持多选功能**,允许在单个附件字段上传和存储多个文件。通过设置 `multiple: true` 属性即可启用。 + +## 字段定义 + +### 多个文件附件 + +```yaml +# expense.object.yml +name: expense +fields: + supporting_docs: + type: file + label: 支持文件 + multiple: true # 启用多选 + accept: ['.pdf', '.docx', '.xlsx'] + max_size: 10485760 # 每个文件最大 10MB +``` + +### 多个图片附件(图库) + +```yaml +# product.object.yml +name: product +fields: + gallery: + type: image + label: 产品图库 + multiple: true # 启用多选 + accept: ['.jpg', '.png', '.webp'] + max_size: 5242880 # 每个图片最大 5MB + max_width: 2000 + max_height: 2000 +``` + +## 数据结构 + +启用 `multiple: true` 后,字段存储的是**数组格式**: + +```json +{ + "id": "exp_001", + "expense_number": "EXP-2024-001", + "supporting_docs": [ + { + "id": "file_001", + "name": "invoice.pdf", + "url": "https://cdn.example.com/files/invoice.pdf", + "size": 123456, + "type": "application/pdf", + "original_name": "发票.pdf", + "uploaded_at": "2024-01-15T10:30:00Z" + }, + { + "id": "file_002", + "name": "contract.pdf", + "url": "https://cdn.example.com/files/contract.pdf", + "size": 234567, + "type": "application/pdf", + "original_name": "合同.pdf", + "uploaded_at": "2024-01-15T10:31:00Z" + }, + { + "id": "file_003", + "name": "receipt.pdf", + "url": "https://cdn.example.com/files/receipt.pdf", + "size": 156789, + "type": "application/pdf", + "original_name": "收据.pdf", + "uploaded_at": "2024-01-15T10:32:00Z" + } + ] +} +``` + +## 上传方式 + +### 方式一:批量上传 API(推荐) + +使用 `/api/files/upload/batch` 端点一次性上传多个文件: + +```javascript +// HTML + + +// JavaScript +async function uploadMultipleFiles() { + const fileInput = document.getElementById('fileInput'); + const formData = new FormData(); + + // 添加所有选中的文件 + for (const file of fileInput.files) { + formData.append('files', file); + } + + formData.append('object', 'expense'); + formData.append('field', 'supporting_docs'); + + // 批量上传 + const uploadRes = await fetch('/api/files/upload/batch', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token + }, + body: formData + }); + + const uploadedFiles = (await uploadRes.json()).data; + // uploadedFiles 是包含所有文件元数据的数组 + + return uploadedFiles; +} +``` + +**cURL 示例:** + +```bash +curl -X POST http://localhost:3000/api/files/upload/batch \ + -H "Authorization: Bearer YOUR_TOKEN" \ + -F "files=@invoice.pdf" \ + -F "files=@contract.pdf" \ + -F "files=@receipt.pdf" \ + -F "object=expense" \ + -F "field=supporting_docs" +``` + +**响应:** + +```json +{ + "data": [ + { + "id": "file_001", + "name": "invoice.pdf", + "url": "https://cdn.example.com/files/uploads/expense/file_001.pdf", + "size": 123456, + "type": "application/pdf", + "original_name": "invoice.pdf", + "uploaded_at": "2024-01-15T10:30:00Z" + }, + { + "id": "file_002", + "name": "contract.pdf", + "url": "https://cdn.example.com/files/uploads/expense/file_002.pdf", + "size": 234567, + "type": "application/pdf", + "original_name": "contract.pdf", + "uploaded_at": "2024-01-15T10:30:01Z" + }, + { + "id": "file_003", + "name": "receipt.pdf", + "url": "https://cdn.example.com/files/uploads/expense/file_003.pdf", + "size": 156789, + "type": "application/pdf", + "original_name": "receipt.pdf", + "uploaded_at": "2024-01-15T10:30:02Z" + } + ] +} +``` + +### 方式二:并行上传多个单文件 + +使用 `/api/files/upload` 端点配合 `Promise.all` 并行上传: + +```javascript +async function uploadMultipleFilesSeparately(files) { + // 并行上传所有文件 + const uploadPromises = Array.from(files).map(async (file) => { + const formData = new FormData(); + formData.append('file', file); + formData.append('object', 'product'); + formData.append('field', 'gallery'); + + const response = await fetch('/api/files/upload', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + token + }, + body: formData + }); + + return (await response.json()).data; + }); + + // 等待所有上传完成 + const uploadedFiles = await Promise.all(uploadPromises); + + return uploadedFiles; +} + +// 使用 +const fileInput = document.getElementById('gallery-input'); +const uploadedImages = await uploadMultipleFilesSeparately(fileInput.files); +``` + +## 创建记录 + +上传完成后,将文件数组传入创建或更新操作: + +```javascript +// 步骤 1:上传多个文件 +const uploadedFiles = await uploadMultipleFiles(); + +// 步骤 2:创建记录,包含所有附件 +const createRes = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + expense_number: 'EXP-2024-001', + amount: 1250.00, + description: '办公用品采购', + supporting_docs: uploadedFiles // 传入文件数组 + } + }) +}); + +const expense = (await createRes.json()); +``` + +## 更新操作 + +### 替换所有附件 + +```javascript +// 上传新的文件集合 +const newFiles = await uploadMultipleFiles(); + +// 替换整个数组 +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'update', + object: 'expense', + args: { + id: 'exp_001', + data: { + supporting_docs: newFiles // 完全替换 + } + } + }) +}); +``` + +### 追加新附件 + +```javascript +// 获取当前记录 +const current = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'findOne', + object: 'expense', + args: 'exp_001' + }) +}).then(r => r.json()); + +// 上传新文件 +const newFiles = await uploadMultipleFiles(); + +// 追加到现有数组 +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'update', + object: 'expense', + args: { + id: 'exp_001', + data: { + supporting_docs: [ + ...(current.supporting_docs || []), + ...newFiles + ] + } + } + }) +}); +``` + +### 删除特定附件 + +```javascript +// 获取当前记录 +const current = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'findOne', + object: 'expense', + args: 'exp_001' + }) +}).then(r => r.json()); + +// 过滤掉要删除的附件(按 id) +const updatedDocs = current.supporting_docs.filter( + doc => doc.id !== 'file_002' // 删除 id 为 file_002 的附件 +); + +// 更新记录 +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'update', + object: 'expense', + args: { + id: 'exp_001', + data: { + supporting_docs: updatedDocs + } + } + }) +}); +``` + +## 查询操作 + +### 查询包含附件的记录 + +```javascript +const expensesWithDocs = await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'find', + object: 'expense', + args: { + filters: [['supporting_docs', '!=', null]], + fields: ['id', 'expense_number', 'supporting_docs'] + } + }) +}).then(r => r.json()); + +// 遍历结果 +expensesWithDocs.items.forEach(expense => { + console.log(`报销单 ${expense.expense_number}:`); + console.log(`- 附件数量: ${expense.supporting_docs?.length || 0}`); + expense.supporting_docs?.forEach(doc => { + console.log(` - ${doc.original_name} (${doc.size} bytes)`); + }); +}); +``` + +## 完整示例:产品图库 + +```javascript +/** + * 完整示例:创建产品并上传多张图片 + */ +async function createProductWithGallery(productData, imageFiles) { + try { + // 步骤 1:批量上传图片 + const formData = new FormData(); + for (const file of imageFiles) { + formData.append('files', file); + } + formData.append('object', 'product'); + formData.append('field', 'gallery'); + + const uploadRes = await fetch('/api/files/upload/batch', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: formData + }); + + if (!uploadRes.ok) { + throw new Error('文件上传失败'); + } + + const uploadedImages = (await uploadRes.json()).data; + + // 步骤 2:创建产品记录 + const createRes = await fetch('/api/objectql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: JSON.stringify({ + op: 'create', + object: 'product', + args: { + ...productData, + gallery: uploadedImages // 图片数组 + } + }) + }); + + if (!createRes.ok) { + throw new Error('产品创建失败'); + } + + const product = (await createRes.json()); + + console.log('产品创建成功:', { + id: product.id, + name: product.name, + 图片数量: product.gallery.length + }); + + return product; + + } catch (error) { + console.error('操作失败:', error); + throw error; + } +} + +// HTML +// + +// 使用 +const fileInput = document.getElementById('gallery-input'); +const product = await createProductWithGallery({ + name: '高端笔记本电脑', + price: 9999.99, + description: '超轻薄设计,高性能处理器' +}, fileInput.files); +``` + +## React 组件示例 + +```tsx +import React, { useState } from 'react'; + +interface MultiFileUploadProps { + objectName: string; + fieldName: string; + onSuccess?: (files: any[]) => void; +} + +export const MultiFileUpload: React.FC = ({ + objectName, + fieldName, + onSuccess +}) => { + const [uploading, setUploading] = useState(false); + const [files, setFiles] = useState([]); + const [error, setError] = useState(null); + + const handleFileChange = (e: React.ChangeEvent) => { + const selectedFiles = Array.from(e.target.files || []); + setFiles(selectedFiles); + setError(null); + }; + + const handleUpload = async () => { + if (files.length === 0) { + setError('请选择文件'); + return; + } + + setUploading(true); + setError(null); + + try { + const formData = new FormData(); + files.forEach(file => { + formData.append('files', file); + }); + formData.append('object', objectName); + formData.append('field', fieldName); + + const response = await fetch('/api/files/upload/batch', { + method: 'POST', + headers: { + 'Authorization': 'Bearer ' + getAuthToken() + }, + body: formData + }); + + if (!response.ok) { + throw new Error('上传失败'); + } + + const { data: uploadedFiles } = await response.json(); + + onSuccess?.(uploadedFiles); + setFiles([]); + + } catch (err) { + setError(err instanceof Error ? err.message : '上传失败'); + } finally { + setUploading(false); + } + }; + + return ( +
+ + + {files.length > 0 && ( +
+

已选择 {files.length} 个文件:

+
    + {files.map((file, index) => ( +
  • + {file.name} ({(file.size / 1024).toFixed(2)} KB) +
  • + ))} +
+
+ )} + + + + {error &&

{error}

} +
+ ); +}; +``` + +## 验证规则 + +多选字段的验证规则: + +- `max_size`: 限制**每个文件**的最大大小 +- `min_size`: 限制**每个文件**的最小大小 +- `accept`: 限制允许的文件类型(对所有文件生效) +- 数组长度:通过应用层逻辑控制(ObjectQL 默认不限制) + +## 总结 + +ObjectQL 的附件字段**完全支持多选功能**: + +✅ **字段定义简单** - 只需设置 `multiple: true` +✅ **两种上传方式** - 批量上传 API 或并行单文件上传 +✅ **数据格式统一** - 单选为对象,多选为数组 +✅ **灵活的更新操作** - 支持替换、追加、删除 +✅ **完整的查询支持** - 可以查询、过滤包含附件的记录 + +**相关文档:** +- [附件 API 完整文档](../api/attachments.md) +- [附件关联指南](./attachment-association-guide-cn.md) +- [文件上传示例](./file-upload-example.md) diff --git a/apps/site/content/docs/examples/s3-integration-guide-cn.mdx b/apps/site/content/docs/examples/s3-integration-guide-cn.mdx new file mode 100644 index 00000000..9b89db55 --- /dev/null +++ b/apps/site/content/docs/examples/s3-integration-guide-cn.mdx @@ -0,0 +1,611 @@ +--- +title: 如何使用 AWS S3 存储附件 +--- + +# 如何使用 AWS S3 存储附件 + +本指南详细说明如何将 ObjectQL 文件附件存储到 AWS S3,包括完整的实现代码和最佳实践。 + +## 目录 + +1. [为什么选择 S3](#为什么选择-s3) +2. [架构设计](#架构设计) +3. [实现方案](#实现方案) +4. [配置说明](#配置说明) +5. [使用示例](#使用示例) +6. [高级功能](#高级功能) +7. [最佳实践](#最佳实践) + +--- + +## 为什么选择 S3 + +使用 AWS S3 存储文件附件有以下优势: + +✅ **可扩展性** - 无限存储容量,无需管理磁盘空间 +✅ **高可用性** - 99.99% 可用性 SLA,自动多区域备份 +✅ **高性能** - 与 CloudFront CDN 集成,全球加速访问 +✅ **成本优化** - 按需付费,支持生命周期管理和存储分层 +✅ **安全性** - 支持加密、访问控制、签名 URL 等安全特性 +✅ **易于维护** - 无需管理服务器,AWS 负责基础设施 + +--- + +## 架构设计 + +### 存储接口抽象 + +ObjectQL 通过 `IFileStorage` 接口实现存储抽象: + +```typescript +interface IFileStorage { + save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise; + get(fileId: string): Promise; + delete(fileId: string): Promise; + getPublicUrl(fileId: string): string; +} +``` + +### S3 集成架构 + +``` +┌─────────────┐ +│ 客户端 │ +│ (Browser) │ +└──────┬──────┘ + │ ① 上传文件 + ▼ +┌─────────────┐ +│ ObjectQL │ +│ Server │──┐ +└──────┬──────┘ │ ② 调用 S3FileStorage + │ │ + │ ▼ + │ ┌─────────────┐ + │ │ S3FileStorage│ + │ │ implements │ + │ │ IFileStorage │ + │ └──────┬───────┘ + │ │ ③ 上传到 S3 + │ ▼ + │ ┌─────────────┐ + │ │ AWS S3 │ + │ │ Bucket │ + │ └──────┬──────┘ + │ │ + │ │ ④ 返回 URL + ▼ ▼ + ┌─────────────────────┐ + │ Database │ + │ (存储文件元数据) │ + └─────────────────────┘ +``` + +--- + +## 实现方案 + +### 1. 安装依赖 + +```bash +npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner +``` + +或使用 pnpm: + +```bash +pnpm add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner +``` + +### 2. 创建 S3FileStorage 类 + +完整实现代码请参考:[s3-storage-implementation.ts](./s3-storage-implementation.ts) + +核心特性: +- ✅ 实现 `IFileStorage` 接口 +- ✅ 支持公开和私有访问模式 +- ✅ 集成 CloudFront CDN +- ✅ 生成签名 URL(临时访问) +- ✅ 支持客户端直传 S3 +- ✅ 自动组织文件夹结构 + +### 3. 关键实现细节 + +#### 文件上传到 S3 + +```typescript +async save(file: Buffer, filename: string, mimeType: string, options?: FileStorageOptions): Promise { + // 生成唯一 ID + const id = crypto.randomBytes(16).toString('hex'); + const ext = filename.substring(filename.lastIndexOf('.')); + + // 构建 S3 key(文件路径) + const folder = options?.folder || 'uploads'; + const objectPath = options?.object ? `${options.object}/` : ''; + const key = `${this.basePrefix}/${folder}/${objectPath}${id}${ext}`; + + // 上传到 S3 + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + Body: file, + ContentType: mimeType, + ACL: this.publicRead ? 'public-read' : 'private', + Metadata: { + 'original-filename': filename, + 'uploaded-by': options?.userId || 'unknown' + } + }); + + await this.s3Client.send(command); + + return { + id: key, + name: `${id}${ext}`, + url: this.getPublicUrl(key), + size: file.length, + type: mimeType, + original_name: filename, + uploaded_at: new Date().toISOString() + }; +} +``` + +#### 文件下载 + +```typescript +async get(fileId: string): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: fileId + }); + + const response = await this.s3Client.send(command); + + // 将流转换为 Buffer + const chunks: Uint8Array[] = []; + for await (const chunk of response.Body) { + chunks.push(chunk); + } + + return Buffer.concat(chunks); +} +``` + +--- + +## 配置说明 + +### 环境变量配置 + +创建 `.env` 文件: + +```bash +# AWS 凭证 +AWS_ACCESS_KEY_ID=your_access_key_id +AWS_SECRET_ACCESS_KEY=your_secret_access_key +AWS_REGION=us-east-1 + +# S3 配置 +S3_BUCKET=my-objectql-uploads +S3_BASE_PREFIX=objectql-uploads + +# CloudFront(可选) +CLOUDFRONT_DOMAIN=https://d123456.cloudfront.net + +# 访问控制 +S3_PUBLIC_READ=false +S3_SIGNED_URL_EXPIRY=3600 +``` + +### 配置类型 + +```typescript +interface S3StorageConfig { + bucket: string; // S3 存储桶名称 + region: string; // AWS 区域 + accessKeyId?: string; // 访问密钥 ID(可选,使用 IAM 角色时) + secretAccessKey?: string; // 访问密钥(可选) + basePrefix?: string; // 文件路径前缀 + cloudFrontDomain?: string; // CloudFront 域名 + publicRead?: boolean; // 是否公开读取 + signedUrlExpiry?: number; // 签名 URL 过期时间(秒) +} +``` + +--- + +## 使用示例 + +### 基础使用 + +```typescript +import { ObjectQL } from '@objectql/core'; +import { createNodeHandler } from '@objectql/server'; +import { S3FileStorage } from './s3-storage-implementation'; + +const app = new ObjectQL({ /* ... */ }); + +// 配置 S3 存储 +const storage = new S3FileStorage({ + bucket: process.env.S3_BUCKET!, + region: process.env.AWS_REGION!, + accessKeyId: process.env.AWS_ACCESS_KEY_ID, + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + basePrefix: process.env.S3_BASE_PREFIX, + cloudFrontDomain: process.env.CLOUDFRONT_DOMAIN, + publicRead: process.env.S3_PUBLIC_READ === 'true' +}); + +// 创建 HTTP 处理器 +const handler = createNodeHandler(app, { fileStorage: storage }); + +// 启动服务器 +const server = http.createServer(handler); +server.listen(3000); +``` + +### 使用 IAM 角色(推荐) + +在 EC2、ECS 或 Lambda 上运行时,推荐使用 IAM 角色而不是硬编码凭证: + +```typescript +const storage = new S3FileStorage({ + bucket: 'my-objectql-uploads', + region: 'us-east-1', + // 不提供 accessKeyId 和 secretAccessKey + // SDK 会自动使用 IAM 角色 + publicRead: false +}); +``` + +需要的 IAM 权限: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::my-objectql-uploads/*", + "arn:aws:s3:::my-objectql-uploads" + ] + } + ] +} +``` + +### 集成 CloudFront CDN + +使用 CloudFront 加速全球访问: + +```typescript +const storage = new S3FileStorage({ + bucket: 'my-objectql-uploads', + region: 'us-east-1', + cloudFrontDomain: 'https://d123456.cloudfront.net', + publicRead: true // CloudFront 需要 +}); +``` + +文件 URL 将是: +``` +https://d123456.cloudfront.net/objectql-uploads/expense/abc123.pdf +``` + +而不是: +``` +https://my-objectql-uploads.s3.us-east-1.amazonaws.com/objectql-uploads/expense/abc123.pdf +``` + +--- + +## 高级功能 + +### 1. 签名 URL(临时访问) + +对于私有文件,生成临时访问链接: + +```typescript +const storage = new S3FileStorage({ + bucket: 'my-objectql-uploads', + region: 'us-east-1', + publicRead: false, // 私有访问 + signedUrlExpiry: 3600 // 1小时过期 +}); + +// 获取签名 URL +const fileId = 'objectql-uploads/expense/abc123.pdf'; +const signedUrl = await storage.getSignedUrl(fileId, 7200); // 2小时有效 + +// 返回给客户端 +res.json({ downloadUrl: signedUrl }); +``` + +### 2. 客户端直传 S3 + +避免文件经过服务器,直接上传到 S3: + +```typescript +// 服务器端:生成上传凭证 +const uploadCredentials = await storage.getSignedUploadUrl( + 'receipt.pdf', + 'application/pdf', + { object: 'expense', userId: 'user_123' } +); + +// 返回给客户端 +res.json(uploadCredentials); +// { +// url: "https://...", +// key: "objectql-uploads/expense/abc123.pdf", +// fields: { "Content-Type": "application/pdf" } +// } +``` + +```javascript +// 客户端:直接上传到 S3 +const formData = new FormData(); +formData.append('key', uploadCredentials.key); +Object.entries(uploadCredentials.fields).forEach(([k, v]) => { + formData.append(k, v); +}); +formData.append('file', fileInput.files[0]); + +await fetch(uploadCredentials.url, { + method: 'PUT', + body: formData +}); + +// 然后创建 ObjectQL 记录 +await fetch('/api/objectql', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + op: 'create', + object: 'expense', + args: { + receipt: { + id: uploadCredentials.key, + url: `https://d123456.cloudfront.net/${uploadCredentials.key}`, + // ... 其他元数据 + } + } + }) +}); +``` + +### 3. 文件夹组织 + +S3FileStorage 自动组织文件夹结构: + +``` +my-objectql-uploads/ +├── objectql-uploads/ # basePrefix +│ ├── uploads/ # 默认文件夹 +│ │ ├── expense/ # 按对象类型 +│ │ │ ├── abc123.pdf +│ │ │ └── def456.jpg +│ │ └── product/ +│ │ ├── img001.jpg +│ │ └── img002.jpg +│ └── avatars/ # 自定义文件夹 +│ └── user_123.jpg +``` + +### 4. 生命周期管理 + +配置 S3 生命周期策略自动管理文件: + +```json +{ + "Rules": [ + { + "Id": "ArchiveOldFiles", + "Status": "Enabled", + "Transitions": [ + { + "Days": 90, + "StorageClass": "STANDARD_IA" + }, + { + "Days": 180, + "StorageClass": "GLACIER" + } + ] + }, + { + "Id": "DeleteTempFiles", + "Status": "Enabled", + "Filter": { + "Prefix": "objectql-uploads/temp/" + }, + "Expiration": { + "Days": 7 + } + } + ] +} +``` + +--- + +## 最佳实践 + +### 1. 安全性 + +**✅ 推荐做法:** + +```typescript +// 使用 IAM 角色 +const storage = new S3FileStorage({ + bucket: 'my-objectql-uploads', + region: 'us-east-1', + publicRead: false // 默认私有 +}); + +// 对需要公开的文件,使用签名 URL +const url = await storage.getSignedUrl(fileId); +``` + +**❌ 避免:** + +```typescript +// 不要在代码中硬编码凭证 +const storage = new S3FileStorage({ + accessKeyId: 'AKIAIOSFODNN7EXAMPLE', // ❌ 危险 + secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY' // ❌ 危险 +}); +``` + +### 2. 性能优化 + +```typescript +// 使用 CloudFront CDN +const storage = new S3FileStorage({ + bucket: 'my-objectql-uploads', + region: 'us-east-1', + cloudFrontDomain: 'https://d123456.cloudfront.net', + publicRead: true +}); + +// 配置 CloudFront 缓存策略 +// - Cache-Control: max-age=31536000 (1年) +// - 启用 Gzip 压缩 +// - 使用边缘位置 +``` + +### 3. 成本优化 + +```typescript +// 1. 使用智能分层存储 +// 在 S3 控制台启用 Intelligent-Tiering + +// 2. 定期清理未使用的文件 +async function cleanupOrphanedFiles() { + // 查询数据库中的所有文件引用 + const usedFiles = await db.query('SELECT file_url FROM attachments'); + + // 列出 S3 中的所有文件 + // 删除未引用的文件 +} + +// 3. 压缩图片 +// 在上传前使用 sharp 库压缩 +import sharp from 'sharp'; + +const compressedBuffer = await sharp(originalBuffer) + .resize(1920, 1080, { fit: 'inside' }) + .jpeg({ quality: 85 }) + .toBuffer(); +``` + +### 4. 监控与日志 + +```typescript +import { S3Client } from '@aws-sdk/client-s3'; + +const s3Client = new S3Client({ + region: 'us-east-1', + logger: console, // 启用日志 +}); + +// CloudWatch 监控指标 +// - NumberOfObjects +// - BucketSizeBytes +// - AllRequests +// - 4xxErrors, 5xxErrors +``` + +### 5. 跨区域复制 + +为灾难恢复配置跨区域复制: + +```json +{ + "Role": "arn:aws:iam::123456789:role/s3-replication-role", + "Rules": [ + { + "Status": "Enabled", + "Priority": 1, + "DeleteMarkerReplication": { "Status": "Enabled" }, + "Destination": { + "Bucket": "arn:aws:s3:::my-objectql-uploads-backup", + "ReplicationTime": { + "Status": "Enabled", + "Time": { "Minutes": 15 } + } + } + } + ] +} +``` + +--- + +## 故障排查 + +### 常见问题 + +**1. AccessDenied 错误** + +``` +Error: Access Denied +``` + +解决方案: +- 检查 IAM 权限是否正确 +- 确认 S3 bucket 策略允许访问 +- 验证 AWS 凭证是否有效 + +**2. NoSuchBucket 错误** + +``` +Error: The specified bucket does not exist +``` + +解决方案: +- 确认 bucket 名称正确 +- 检查 region 配置是否匹配 +- 在 AWS 控制台创建 bucket + +**3. 上传缓慢** + +解决方案: +- 使用客户端直传 S3 +- 启用 S3 Transfer Acceleration +- 选择地理位置更近的 region + +**4. 文件无法访问** + +解决方案: +- 检查 ACL 设置(public-read vs private) +- 对私有文件使用签名 URL +- 验证 CloudFront 分发配置 + +--- + +## 总结 + +通过实现 `IFileStorage` 接口,ObjectQL 可以无缝集成 AWS S3 存储。关键要点: + +✅ **简单集成** - 只需实现 4 个方法 +✅ **灵活配置** - 支持公开/私有访问、CDN、签名 URL +✅ **生产就绪** - 内置错误处理、元数据管理 +✅ **可扩展** - 支持客户端直传、生命周期管理 + +完整代码示例:[s3-storage-implementation.ts](./s3-storage-implementation.ts) + +--- + +**相关文档:** +- [AWS S3 官方文档](https://docs.aws.amazon.com/s3/) +- [AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/) +- [ObjectQL 附件 API 文档](../api/attachments.md) diff --git a/apps/site/content/docs/examples/s3-storage-implementation.ts b/apps/site/content/docs/examples/s3-storage-implementation.ts new file mode 100644 index 00000000..4a6f46a2 --- /dev/null +++ b/apps/site/content/docs/examples/s3-storage-implementation.ts @@ -0,0 +1,241 @@ +/** + * ObjectQL + * Copyright (c) 2026-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { IFileStorage, AttachmentData, FileStorageOptions } from '@objectql/server'; +import { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import * as crypto from 'crypto'; + +/** + * AWS S3 Storage Configuration + */ +export interface S3StorageConfig { + /** S3 Bucket name */ + bucket: string; + /** AWS Region (e.g., 'us-east-1', 'ap-southeast-1') */ + region: string; + /** AWS Access Key ID (optional if using IAM roles) */ + accessKeyId?: string; + /** AWS Secret Access Key (optional if using IAM roles) */ + secretAccessKey?: string; + /** Base path/prefix for all files (optional) */ + basePrefix?: string; + /** CloudFront distribution domain (optional, for CDN) */ + cloudFrontDomain?: string; + /** Enable public read access (default: false) */ + publicRead?: boolean; + /** Signed URL expiration time in seconds (default: 3600) */ + signedUrlExpiry?: number; +} + +/** + * AWS S3 Storage Implementation for ObjectQL + * + * @example + * ```typescript + * const storage = new S3FileStorage({ + * bucket: 'my-objectql-uploads', + * region: 'us-east-1', + * accessKeyId: process.env.AWS_ACCESS_KEY_ID, + * secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, + * cloudFrontDomain: 'https://d123456.cloudfront.net', // optional + * publicRead: true + * }); + * + * const handler = createNodeHandler(app, { fileStorage: storage }); + * ``` + */ +export class S3FileStorage implements IFileStorage { + private s3Client: S3Client; + private bucket: string; + private basePrefix: string; + private cloudFrontDomain?: string; + private publicRead: boolean; + private signedUrlExpiry: number; + + constructor(config: S3StorageConfig) { + this.bucket = config.bucket; + this.basePrefix = config.basePrefix || 'objectql-uploads'; + this.cloudFrontDomain = config.cloudFrontDomain; + this.publicRead = config.publicRead ?? false; + this.signedUrlExpiry = config.signedUrlExpiry ?? 3600; + + // Initialize S3 Client + this.s3Client = new S3Client({ + region: config.region, + credentials: config.accessKeyId && config.secretAccessKey ? { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey + } : undefined // Use IAM role if credentials not provided + }); + } + + async save( + file: Buffer, + filename: string, + mimeType: string, + options?: FileStorageOptions + ): Promise { + // Generate unique file ID + const id = crypto.randomBytes(16).toString('hex'); + const ext = filename.substring(filename.lastIndexOf('.')); + + // Build S3 key with organized structure + const folder = options?.folder || 'uploads'; + const objectPath = options?.object ? `${options.object}/` : ''; + const key = `${this.basePrefix}/${folder}/${objectPath}${id}${ext}`; + + // Upload to S3 + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + Body: file, + ContentType: mimeType, + ACL: this.publicRead ? 'public-read' : 'private', + Metadata: { + 'original-filename': filename, + 'uploaded-by': options?.userId || 'unknown', + 'object-type': options?.object || 'general' + } + }); + + await this.s3Client.send(command); + + // Generate public URL + const url = this.getPublicUrl(key); + + return { + id: key, // Use S3 key as ID for easy retrieval + name: `${id}${ext}`, + url, + size: file.length, + type: mimeType, + original_name: filename, + uploaded_at: new Date().toISOString(), + uploaded_by: options?.userId + }; + } + + async get(fileId: string): Promise { + try { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: fileId + }); + + const response = await this.s3Client.send(command); + + // Convert stream to buffer + const chunks: Uint8Array[] = []; + if (response.Body) { + const stream = response.Body as any; + for await (const chunk of stream) { + chunks.push(chunk); + } + } + + return Buffer.concat(chunks); + } catch (error: any) { + if (error.name === 'NoSuchKey') { + return null; + } + throw error; + } + } + + async delete(fileId: string): Promise { + try { + const command = new DeleteObjectCommand({ + Bucket: this.bucket, + Key: fileId + }); + + await this.s3Client.send(command); + return true; + } catch (error) { + console.error('Error deleting file from S3:', error); + return false; + } + } + + getPublicUrl(fileId: string): string { + // Use CloudFront if configured + if (this.cloudFrontDomain) { + return `${this.cloudFrontDomain}/${fileId}`; + } + + // Use S3 direct URL if public + if (this.publicRead) { + const region = this.s3Client.config.region; + return `https://${this.bucket}.s3.${region}.amazonaws.com/${fileId}`; + } + + // For private files, return placeholder (actual signed URL should be generated on demand) + return `s3://${this.bucket}/${fileId}`; + } + + /** + * Generate a signed URL for temporary access to a private file + * @param fileId S3 key of the file + * @param expiresIn Expiration time in seconds (default: configured expiry) + */ + async getSignedUrl(fileId: string, expiresIn?: number): Promise { + const command = new GetObjectCommand({ + Bucket: this.bucket, + Key: fileId + }); + + const url = await getSignedUrl( + this.s3Client, + command, + { expiresIn: expiresIn || this.signedUrlExpiry } + ); + + return url; + } + + /** + * Get signed upload URL for direct client-to-S3 uploads + * @param filename Original filename + * @param mimeType File MIME type + * @param options Storage options + */ + async getSignedUploadUrl( + filename: string, + mimeType: string, + options?: FileStorageOptions + ): Promise<{ url: string; key: string; fields: Record }> { + const id = crypto.randomBytes(16).toString('hex'); + const ext = filename.substring(filename.lastIndexOf('.')); + + const folder = options?.folder || 'uploads'; + const objectPath = options?.object ? `${options.object}/` : ''; + const key = `${this.basePrefix}/${folder}/${objectPath}${id}${ext}`; + + const command = new PutObjectCommand({ + Bucket: this.bucket, + Key: key, + ContentType: mimeType, + ACL: this.publicRead ? 'public-read' : 'private' + }); + + const url = await getSignedUrl( + this.s3Client, + command, + { expiresIn: 3600 } // 1 hour + ); + + return { + url, + key, + fields: { + 'Content-Type': mimeType + } + }; + } +} diff --git a/apps/site/content/docs/guide/architecture/overview.mdx b/apps/site/content/docs/guide/architecture/overview.mdx new file mode 100644 index 00000000..78a27704 --- /dev/null +++ b/apps/site/content/docs/guide/architecture/overview.mdx @@ -0,0 +1,38 @@ +--- +title: Architecture & Concepts +--- + +# Architecture & Concepts + +ObjectQL is built with a modular architecture that separates the data definition (Metadata) from the execution engine (Driver). This design allows applications to run on different database stacks (SQL vs NoSQL) without changing the business logic. + +## High-Level Overview + +An ObjectQL application consists of three main layers: + +1. **Metadata Layer**: JSON/YAML files that define the shape of your data (`.object.yml`) and operations. +2. **Core Engine**: The `ObjectQL` class that loads metadata, validates requests, and orchestrates execution. +3. **Driver Layer**: Adapters that translate ObjectQL requests into database-specific queries (SQL, Mongo Protocol, etc.). + +## Dependency Graph + +The project is structured as a monorepo with strict dependency rules to ensure scalability and maintainability. + +* **`@objectql/types`**: The shared contract. Contains all interfaces (`ObjectConfig`, `ObjectQLDriver`). Has **zero dependencies**. +* **`@objectql/core`**: The main runtime. It depends on `types`. +* **`@objectql/driver-*`**: Database adapters. They implement interfaces from `types` but do **not** depend on `core` (avoiding circular dependencies). + +## Core Concepts + +### 1. Metadata-First +In traditional ORMs (like TypeORM or Prisma), you define classes/schema in code. In ObjectQL, the schema is data itself. This allows: +* Dynamic schema generation at runtime. +* Building "No-Code" table designers. +* Hot-reloading of data structure without recompiling. + +### 2. Universal Protocol +ObjectQL uses a unified JSON-based query language (AST). This allows frontends, AI agents, or external systems to send complex queries (`filters`, `expand`, `aggregates`) in a safe, serializable format. + +### 3. Logic: Actions & Hooks +* **Hooks**: Intercept standard CRUD operations (e.g., "Before Create", "After Update") to enforce business rules. +* **Actions**: Define custom RPC methods (e.g., "Approve Invoice") exposed via the API. diff --git a/apps/site/content/docs/guide/architecture/why-objectql.mdx b/apps/site/content/docs/guide/architecture/why-objectql.mdx new file mode 100644 index 00000000..8339d7ce --- /dev/null +++ b/apps/site/content/docs/guide/architecture/why-objectql.mdx @@ -0,0 +1,58 @@ +--- +title: Why ObjectQL? +--- + +# Why ObjectQL? + +In the era of AI automation, the requirements for backend infrastructure have shifted. We are no longer just building for human users on web forms; we are building systems that **AI Agents** need to read, understand, and manipulate. + +## The Problem with Traditional ORMs + +Tools like TypeORM, Prisma, or Sequelize are fantastic for human developers. They rely heavily on: +1. **Complex TypeScript Types**: Great for IDE autocomplete, but invisible to an LLM running in a production execution environment. +2. **Chained Method Calls**: `db.users.where(...).include(...)`. This requires the AI to synthesize valid executable code, which is prone to syntax errors and hallucinations. +3. **Code-First Schema**: The schema is buried in class definitions, making it hard to extract a simple "map" of the data for the AI context window. + +## The ObjectQL Solution: Protocol-First + +ObjectQL treats **Logic as Data**. + +### 1. Schema is JSON (The Context) +Instead of parsing TypeScript files, an AI Agent can simply read a JSON definition. This is the native tongue of LLMs. + +```json +{ + "name": "contact", + "fields": { "email": { "type": "text", "required": true } } +} +``` + +This compact format fits perfectly in RAG (Retrieval-Augmented Generation) contexts. + +### 2. Queries are ASTs (The Action) +To fetch data, the AI doesn't need to write SQL or function code. It generates a JSON object (Abstract Syntax Tree). + +```json +{ + "op": "find", + "from": "contact", + "filters": [["email", "contains", "@gmail.com"]] +} +``` +* **Safe**: No SQL Injection possible. The Driver handles escaping. +* **Deterministic**: It either parses or it fails. No subtle logic bugs from misplaced parentheses in code. +* **Portability**: The same JSON runs on Mongo, Postgres, or a REST API. + +## Comparison + +| Feature | Traditional ORM | ObjectQL | +| :--- | :--- | :--- | +| **Schema Definition** | TypeScript Classes / Migration Files | JSON / YAML Metadata | +| **Query Language** | Fluent API / Raw SQL | JSON AST | +| **Runtime** | Node.js / Python Runtime | Universal Protocol (Any Language) | +| **AI Friendliness** | Low (Requires Code Gen) | High (Requires JSON Gen) | +| **Dynamic Fields** | Hard (Migration required) | Native (Metadata-driven) | + +## Conclusion + +If you are building a Copilot, an Autonomous Agent, or a Low-Code platform, ObjectQL provides the structured, safe, and descriptive layer that connects your LLM to your database. diff --git a/apps/site/content/docs/guide/cli.mdx b/apps/site/content/docs/guide/cli.mdx new file mode 100644 index 00000000..97e5b572 --- /dev/null +++ b/apps/site/content/docs/guide/cli.mdx @@ -0,0 +1,286 @@ +--- +title: CLI Guide +--- + +# CLI Guide + +The ObjectQL CLI (`@objectql/cli`) is an essential tool for development, automating tasks like type generation and database migrations. + +## 1. Installation + +The CLI is typically installed as a dev dependency in your project. + +```bash +npm install -D @objectql/cli +``` + +You can then run it via `npx objectql` or add scripts to your `package.json`. + +## 2. Core Commands + +### 2.1 `init` (Create Project) + +The recommended way to create a new ObjectQL project is using the initialization package. + +```bash +npm create @objectql@latest [name] [options] +``` + +**Options:** + +| Option | Alias | Default | Description | +| :--- | :--- | :--- | :--- | +| `--template