diff --git a/openapi/COMMERCIAL.md b/openapi/COMMERCIAL.md new file mode 100644 index 00000000..7281e72b --- /dev/null +++ b/openapi/COMMERCIAL.md @@ -0,0 +1,107 @@ +# Commercial Usage Declaration - Yao OpenAPI + +This document outlines the commercial usage terms for the Yao OpenAPI components contained within this directory. + +## Commercial License Requirements + +The Yao OpenAPI module is available for commercial use under the following **mandatory conditions**: + +### 1. Developer Certificate Requirement + +**Commercial usage requires obtaining a Yao Developer Certificate.** + +- All commercial deployments of Yao OpenAPI must possess a valid Yao Developer Certificate +- The certificate must be obtained through official Yao certification channels +- Usage without proper certification constitutes license violation + +### 2. Trademark and Branding Restrictions + +**Yao branding elements are protected and must remain intact:** + +- Yao App Engine logos and copyright information **MUST NOT** be removed or modified +- All Yao trademark elements must remain visible and unaltered in production deployments +- Logo and copyright modifications are **ONLY** permitted with explicit authorization through Yao Developer Certificate + +### 3. Certificate Verification System Integrity + +**The Yao certificate verification system must be maintained in its entirety:** + +- **PROHIBITED**: Removal, modification, or circumvention of Yao certificate verification logic +- **PROHIBITED**: Alteration of certificate validation processes and related code components +- **PROHIBITED**: Modification of certificate verification pages (marked in code comments) +- **REQUIRED**: Complete preservation of the Yao certificate verification system regardless of usage purpose + +### 4. Code Integrity Requirements + +For commercial deployments, the following **core verification mechanisms** must remain unmodified: + +- Certificate validation endpoints and middleware +- Authentication verification logic and OAuth implementation +- License verification routines and related authentication flows +- Commercial license checking and enforcement code +- Yao branding and copyright verification systems + +_Note: General business logic and API customizations are permitted, but the core licensing and certificate verification infrastructure must remain intact._ + +## Compliance Obligations + +### For Commercial Users: + +1. **Obtain Valid Certification**: Acquire and maintain a current Yao Developer Certificate +2. **Preserve Verification Logic**: Maintain all certificate verification code intact +3. **Respect Trademark Rights**: Keep all Yao branding elements unmodified +4. **Regular Compliance Review**: Ensure ongoing adherence to certification requirements + +### For Developers and Integrators: + +1. **Code Review Responsibility**: Verify that certificate verification logic remains intact +2. **Documentation Compliance**: Maintain all certificate-related documentation and comments +3. **Deployment Verification**: Ensure production deployments include proper certificate validation +4. **Update Compliance**: When updating Yao components, preserve all certificate verification systems + +## Prohibited Activities + +The following activities are **strictly prohibited** without explicit written authorization: + +- Removing or disabling certificate verification mechanisms +- Modifying Yao branding, logos, or copyright notices +- Circumventing license validation processes +- Redistributing modified versions that bypass certificate requirements +- Creating derivative works that remove authentication restrictions + +## Legal Compliance + +This commercial usage declaration is governed by: + +- **Base License**: Modified Apache License 2.0 +- **Additional Terms**: As specified in the root LICENSE file +- **Commercial Requirements**: As outlined in this document +- **Jurisdiction**: Subject to applicable intellectual property and software licensing laws + +## Violation Consequences + +Failure to comply with these commercial usage terms may result in: + +- Immediate license termination +- Legal action for intellectual property infringement +- Liability for damages and legal costs +- Permanent prohibition from commercial Yao ecosystem usage + +## Contact and Authorization + +For commercial licensing inquiries, certificate applications, or clarification of usage terms: + +- **Official Website**: [Yao Developer Certification Portal] +- **License Inquiries**: Contact authorized Yao licensing representatives +- **Technical Support**: Available exclusively to certified commercial users + +--- + +**Important Notice**: This commercial declaration supplements and does not replace the terms specified in the root LICENSE file. In case of conflicts, the most restrictive terms apply. + +**© 2025 Infinite Wisdom Software. All rights reserved.** + +--- + +_Last Updated: 2025-07-22_ +_Document Version: 1.0_ diff --git a/openapi/COMMERCIAL.zh-CN.md b/openapi/COMMERCIAL.zh-CN.md new file mode 100644 index 00000000..80652f9d --- /dev/null +++ b/openapi/COMMERCIAL.zh-CN.md @@ -0,0 +1,107 @@ +# 商业使用声明 - Yao OpenAPI + +本文档阐述了本目录下 Yao OpenAPI 组件的商业使用条款。 + +## 商业许可证要求 + +Yao OpenAPI 模块可用于商业用途,但须遵循以下**强制性条件**: + +### 1. 开发者证书要求 + +**商业使用需要获得 Yao 开发者证书。** + +- 所有 Yao OpenAPI 的商业部署都必须拥有有效的 Yao 开发者证书 +- 证书必须通过官方 Yao 认证渠道获得 +- 未经适当认证的使用构成许可证违规 + +### 2. 商标和品牌限制 + +**Yao 品牌元素受保护,必须保持完整:** + +- Yao App Engine 标志和版权信息**不得**被移除或修改 +- 所有 Yao 商标元素必须在生产部署中保持可见且未改动 +- 标志和版权修改**仅**在通过 Yao 开发者证书获得明确授权后允许 + +### 3. 证书验证系统完整性 + +**Yao 证书验证系统必须完整保持:** + +- **禁止**:移除、修改或绕过 Yao 证书验证逻辑 +- **禁止**:更改证书验证流程和相关代码组件 +- **禁止**:修改证书验证页面(在代码注释中标记) +- **要求**:无论使用目的如何,必须完整保持 Yao 证书验证系统 + +### 4. 代码完整性要求 + +对于商业部署,以下**核心验证机制**必须保持未修改状态: + +- 证书验证端点和中间件 +- 认证验证逻辑和 OAuth 实现 +- 许可证验证例程和相关认证流程 +- 商业许可证检查和执行代码 +- Yao 品牌和版权验证系统 + +_注意:一般业务逻辑和 API 定制是允许的,但核心许可证和证书验证基础设施必须保持完整。_ + +## 合规义务 + +### 商业用户: + +1. **获得有效认证**:获得并维护当前有效的 Yao 开发者证书 +2. **保持验证逻辑**:保持所有证书验证代码完整 +3. **尊重商标权**:保持所有 Yao 品牌元素未修改 +4. **定期合规审查**:确保持续遵守认证要求 + +### 开发者和集成商: + +1. **代码审查责任**:验证证书验证逻辑保持完整 +2. **文档合规**:维护所有与证书相关的文档和注释 +3. **部署验证**:确保生产部署包含适当的证书验证 +4. **更新合规**:更新 Yao 组件时,保持所有证书验证系统 + +## 禁止活动 + +以下活动在没有明确书面授权的情况下**严格禁止**: + +- 移除或禁用证书验证机制 +- 修改 Yao 品牌、标志或版权声明 +- 绕过许可证验证流程 +- 重新分发绕过证书要求的修改版本 +- 创建移除认证限制的衍生作品 + +## 法律合规 + +本商业使用声明受以下条款管辖: + +- **基础许可证**:修改版 Apache License 2.0 +- **附加条款**:如根目录 LICENSE 文件中指定 +- **商业要求**:如本文档中概述 +- **管辖权**:受适用的知识产权和软件许可法律约束 + +## 违规后果 + +未能遵守这些商业使用条款可能导致: + +- 立即终止许可证 +- 因知识产权侵权而提起法律诉讼 +- 损害赔偿和法律费用责任 +- 永久禁止商业 Yao 生态系统使用 + +## 联系和授权 + +如需商业许可咨询、证书申请或使用条款澄清: + +- **官方网站**:[Yao 开发者认证门户] +- **许可证咨询**:联系授权的 Yao 许可代表 +- **技术支持**:仅向经认证的商业用户提供 + +--- + +**重要声明**:本商业声明是对根目录 LICENSE 文件中指定条款的补充,不能替代这些条款。如有冲突,以最严格的条款为准。 + +**© 2025 Infinite Wisdom Software. 保留所有权利。** + +--- + +_最后更新:2025-07-22_ +_文档版本:1.0_ diff --git a/openapi/README.md b/openapi/README.md index 0c3e62ff..97843da3 100644 --- a/openapi/README.md +++ b/openapi/README.md @@ -1,9 +1,400 @@ # Yao OpenAPI -## OAuth +The Yao OpenAPI provides a comprehensive set of RESTful APIs for managing Yao applications, including OAuth 2.1/OpenID Connect authentication, DSL resource management, and development utilities. -## Agent +## Base URL -## Knowledge Base +All API endpoints are prefixed with a configurable base URL (e.g., `/v1`). -## Audit Log +## Authentication + +The Yao OpenAPI implements OAuth 2.1 and OpenID Connect Core 1.0 specifications for secure authentication and authorization. + +### Supported Grant Types + +- **Authorization Code Flow** - RFC 6749 (recommended for web applications) +- **Client Credentials Flow** - RFC 6749 (for server-to-server communication) +- **Device Authorization Flow** - RFC 8628 (for devices with limited input) +- **Refresh Token Flow** - RFC 6749 (for token renewal) +- **Token Exchange** - RFC 8693 (for token delegation) + +### Discovery Endpoints + +The OpenAPI server provides standard OAuth 2.1 discovery endpoints: + +``` +GET /.well-known/oauth-authorization-server +``` + +Returns server metadata including supported endpoints, grant types, and security features. + +### OAuth Endpoints + +#### Authorization Endpoint + +Initiate the authorization code flow: + +``` +GET /oauth/authorize?client_id={client_id}&response_type=code&redirect_uri={redirect_uri}&scope={scope}&state={state} +``` + +**Parameters:** + +- `client_id` (required): Client identifier +- `response_type` (required): Must be "code" +- `redirect_uri` (required): Client redirect URI +- `scope` (optional): Requested scopes +- `state` (recommended): CSRF protection state parameter +- `code_challenge` (optional): PKCE code challenge +- `code_challenge_method` (optional): PKCE challenge method + +#### Token Endpoint + +Exchange authorization code for access token: + +``` +POST /oauth/token +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code&code={code}&redirect_uri={redirect_uri}&client_id={client_id}&client_secret={client_secret} +``` + +**Client Credentials Flow:** + +``` +POST /oauth/token +Content-Type: application/x-www-form-urlencoded + +grant_type=client_credentials&client_id={client_id}&client_secret={client_secret}&scope={scope} +``` + +**Response:** + +```json +{ + "access_token": "eyJhbGciOiJSUzI1NiIs...", + "token_type": "Bearer", + "expires_in": 3600, + "refresh_token": "def50200...", + "scope": "openid profile" +} +``` + +#### Token Introspection + +Validate and inspect access tokens (RFC 7662): + +``` +POST /oauth/introspect +Content-Type: application/x-www-form-urlencoded +Authorization: Basic {base64(client_id:client_secret)} + +token={access_token} +``` + +**Response:** + +```json +{ + "active": true, + "scope": "openid profile", + "client_id": "your_client_id", + "username": "user@example.com", + "token_type": "Bearer", + "exp": 1640995200, + "iat": 1640991600 +} +``` + +#### Token Revocation + +Revoke access or refresh tokens (RFC 7009): + +``` +POST /oauth/revoke +Content-Type: application/x-www-form-urlencoded +Authorization: Basic {base64(client_id:client_secret)} + +token={token}&token_type_hint={access_token|refresh_token} +``` + +#### Dynamic Client Registration + +Register OAuth clients dynamically (RFC 7591): + +``` +POST /oauth/register +Content-Type: application/json + +{ + "client_name": "My Application", + "redirect_uris": ["https://app.example.com/callback"], + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "scope": "openid profile" +} +``` + +**Response:** + +```json +{ + "client_id": "generated_client_id", + "client_secret": "generated_client_secret", + "client_name": "My Application", + "redirect_uris": ["https://app.example.com/callback"], + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "scope": "openid profile" +} +``` + +#### JSON Web Key Set + +Retrieve public keys for token verification (RFC 7517): + +``` +GET /oauth/jwks +``` + +**Response:** + +```json +{ + "keys": [ + { + "kty": "RSA", + "kid": "key-id-1", + "use": "sig", + "n": "...", + "e": "AQAB" + } + ] +} +``` + +### Authentication Usage + +#### Bearer Token Authentication + +Include the access token in API requests: + +```bash +curl -X GET "/v1/dsl/list/model" \ + -H "Authorization: Bearer {access_token}" +``` + +#### Client Credentials + +For server-to-server authentication, use the client credentials flow to obtain an access token, then include it in subsequent API requests. + +## Hello World API + +Simple endpoints for testing connectivity and authentication. + +### Public Endpoint + +Test basic connectivity without authentication: + +``` +GET /helloworld/public +POST /helloworld/public +``` + +**Response:** + +```json +{ + "MESSAGE": "HELLO, WORLD", + "SERVER_TIME": "2024-01-15T10:30:00Z", + "VERSION": "1.0.0", + "PRVERSION": "1.0.0-preview", + "CUI": "1.0.0", + "PRCUI": "1.0.0-preview", + "APP": "YaoApp", + "APP_VERSION": "1.0.0" +} +``` + +### Protected Endpoint + +Test OAuth authentication: + +``` +GET /helloworld/protected +POST /helloworld/protected +``` + +**Headers:** + +``` +Authorization: Bearer {access_token} +``` + +**Response:** +Same as public endpoint, but requires valid authentication. + +**Example:** + +```bash +# Get access token first +curl -X POST "/v1/oauth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&client_id=your_client&client_secret=your_secret" + +# Use token to access protected endpoint +curl -X GET "/v1/helloworld/protected" \ + -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." +``` + +## DSL Management API + +Comprehensive API for managing Yao DSL resources (models, connectors, MCP clients, etc.). + +**[View Full DSL API Documentation →](dsl/README.md)** + +The DSL Management API provides: + +- **Resource Management**: Create, read, update, delete DSL resources +- **Load Management**: Load, unload, reload DSL resources +- **Validation**: Validate DSL source code syntax +- **Execution**: Execute methods on loaded DSL resources +- **Discovery**: List and inspect available DSL resources + +**Key Endpoints:** + +- `GET /dsl/list/{type}` - List DSL resources +- `POST /dsl/create/{type}` - Create new DSL resource +- `GET /dsl/inspect/{type}/{id}` - Inspect DSL resource details +- `PUT /dsl/update/{type}` - Update existing DSL resource +- `DELETE /dsl/delete/{type}/{id}` - Delete DSL resource + +All DSL endpoints require OAuth authentication. + +## Error Responses + +All endpoints return standardized error responses: + +```json +{ + "error": "invalid_request", + "error_description": "The request is missing a required parameter" +} +``` + +**Common HTTP Status Codes:** + +- `200` - Success +- `201` - Created +- `400` - Bad Request (invalid parameters) +- `401` - Unauthorized (authentication required) +- `403` - Forbidden (insufficient permissions) +- `404` - Not Found +- `500` - Internal Server Error + +**OAuth Error Codes:** + +- `invalid_request` - Request is malformed +- `invalid_client` - Client authentication failed +- `invalid_grant` - Grant is invalid or expired +- `unauthorized_client` - Client not authorized for grant type +- `unsupported_grant_type` - Grant type not supported +- `invalid_scope` - Requested scope is invalid + +## Security Features + +The OpenAPI implements comprehensive security measures: + +### OAuth 2.1 Security + +- **PKCE (Proof Key for Code Exchange)** - Required for public clients +- **State Parameter** - CSRF protection for authorization requests +- **Secure Token Storage** - Access tokens with appropriate expiration +- **Client Authentication** - Multiple authentication methods supported + +### HTTP Security Headers + +All responses include security headers: + +- `Cache-Control: no-store, no-cache, must-revalidate` +- `Pragma: no-cache` +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: DENY` + +### Rate Limiting + +API endpoints are protected against abuse with configurable rate limiting. + +## Example Workflows + +### Web Application Authentication + +1. **Register your client** (if using dynamic registration): + +```bash +curl -X POST "/v1/oauth/register" \ + -H "Content-Type: application/json" \ + -d '{ + "client_name": "My Web App", + "redirect_uris": ["https://myapp.com/callback"], + "grant_types": ["authorization_code", "refresh_token"] + }' +``` + +2. **Initiate authorization flow**: + +``` +https://api.example.com/v1/oauth/authorize?client_id=your_client_id&response_type=code&redirect_uri=https://myapp.com/callback&scope=openid+profile&state=random_state +``` + +3. **Exchange authorization code for tokens**: + +```bash +curl -X POST "/v1/oauth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=authorization_code&code=auth_code&redirect_uri=https://myapp.com/callback&client_id=your_client_id&client_secret=your_secret" +``` + +4. **Use access token to call APIs**: + +```bash +curl -X GET "/v1/dsl/list/model" \ + -H "Authorization: Bearer access_token_here" +``` + +### Server-to-Server Integration + +1. **Obtain client credentials token**: + +```bash +curl -X POST "/v1/oauth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&client_id=server_client&client_secret=server_secret&scope=dsl:manage" +``` + +2. **Manage DSL resources**: + +```bash +# Create a new model +curl -X POST "/v1/dsl/create/model" \ + -H "Authorization: Bearer {access_token}" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "product", + "source": "{ \"name\": \"product\", \"table\": { \"name\": \"products\" }, \"columns\": [...] }" + }' +``` + +## Configuration + +The OpenAPI server is configured through `openapi/openapi.yao`. + +**[View Complete Configuration Examples →](https://github.com/YaoApp/yao-dev-app/tree/main/openapi)** + +This includes comprehensive configuration examples for: + +- OAuth 2.1 server settings +- Client registration and management +- Security and authentication policies +- Development and production environments +- API endpoint configuration diff --git a/openapi/dsl/README.md b/openapi/dsl/README.md new file mode 100644 index 00000000..70538e89 --- /dev/null +++ b/openapi/dsl/README.md @@ -0,0 +1,490 @@ +# DSL Management API + +This document describes the RESTful API for managing Yao DSL resources (models, connectors, MCP clients, etc.). + +## Base URL + +All endpoints are prefixed with the configured base URL followed by `/dsl` (e.g., `/v1/dsl`). + +## Authentication + +All endpoints require OAuth authentication via the configured OAuth provider. + +## DSL Types + +Supported DSL types: + +- `model` - Database models +- `connector` - External service connectors +- `mcp-client` - MCP client configurations +- `api` - HTTP API definitions + +## Endpoints + +### Information Endpoints + +#### Inspect DSL + +Get detailed information about a DSL resource. + +``` +GET /inspect/{type}/{id} +``` + +**Parameters:** + +- `type` (path): DSL type +- `id` (path): DSL identifier + +**Example:** + +```bash +curl -X GET "/v1/dsl/inspect/model/user" \ + -H "Authorization: Bearer {token}" +``` + +**Response:** + +```json +{ + "id": "user", + "type": "model", + "label": "User Model", + "description": "User management model", + "tags": ["auth", "user"], + "path": "models/user.mod.yao", + "store": "file", + "status": "loaded", + "readonly": false, + "builtin": false, + "mtime": "2024-01-15T10:30:00Z", + "ctime": "2024-01-10T09:00:00Z" +} +``` + +#### Get DSL Source Code + +Retrieve the source code of a DSL resource. + +``` +GET /source/{type}/{id} +``` + +**Example:** + +```bash +curl -X GET "/v1/dsl/source/model/user" \ + -H "Authorization: Bearer {token}" +``` + +**Response:** + +```json +{ + "source": "{\n \"name\": \"user\",\n \"table\": {\n \"name\": \"users\"\n },\n \"columns\": [...]\n}" +} +``` + +#### Get DSL File Path + +Get the file system path for a DSL resource. + +``` +GET /path/{type}/{id} +``` + +**Response:** + +```json +{ + "path": "models/user.mod.yao" +} +``` + +#### List DSLs + +List DSL resources with optional filtering. + +``` +GET /list/{type}?sort={sort}&order={order}&store={store}&source={source}&tags={tags}&pattern={pattern} +``` + +**Query Parameters:** + +- `sort` (optional): Sort field +- `order` (optional): Sort order ("asc" or "desc") +- `store` (optional): Storage type filter ("db" or "file") +- `source` (optional): Include source code in response (true/false) +- `tags` (optional): Comma-separated list of tags to filter by +- `pattern` (optional): File name pattern matching + +**Example:** + +```bash +curl -X GET "/v1/dsl/list/model?store=file&tags=user,auth" \ + -H "Authorization: Bearer {token}" +``` + +**Response:** + +```json +[ + { + "id": "user", + "type": "model", + "label": "User Model", + "description": "User management model", + "tags": ["auth", "user"], + "path": "models/user.mod.yao", + "store": "file", + "status": "loaded" + } +] +``` + +#### Check DSL Existence + +Check if a DSL resource exists. + +``` +GET /exists/{type}/{id} +``` + +**Response:** + +```json +{ + "exists": true +} +``` + +### CRUD Operations + +#### Create DSL + +Create a new DSL resource. + +``` +POST /create/{type} +``` + +**Request Body:** + +```json +{ + "id": "test_user", + "source": "{\n \"name\": \"test_user\",\n \"table\": {\n \"name\": \"test_users\",\n \"comment\": \"Test User\"\n },\n \"columns\": [\n { \"name\": \"id\", \"type\": \"ID\" },\n { \"name\": \"name\", \"type\": \"string\", \"length\": 80 }\n ]\n}", + "store": "file" +} +``` + +**Example:** + +```bash +curl -X POST "/v1/dsl/create/model" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "test_user", + "source": "{ \"name\": \"test_user\", \"table\": { \"name\": \"test_users\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }] }", + "store": "file" + }' +``` + +**Response:** + +```json +{ + "message": "DSL created successfully" +} +``` + +#### Update DSL + +Update an existing DSL resource. + +``` +PUT /update/{type} +``` + +**Request Body:** + +```json +{ + "id": "test_user", + "source": "{\n \"name\": \"test_user\",\n \"table\": {\n \"name\": \"test_users\",\n \"comment\": \"Updated Test User\"\n },\n \"columns\": [\n { \"name\": \"id\", \"type\": \"ID\" },\n { \"name\": \"name\", \"type\": \"string\", \"length\": 100 }\n ]\n}" +} +``` + +**Response:** + +```json +{ + "message": "DSL updated successfully" +} +``` + +#### Delete DSL + +Delete a DSL resource. + +``` +DELETE /delete/{type}/{id} +``` + +**Example:** + +```bash +curl -X DELETE "/v1/dsl/delete/model/test_user" \ + -H "Authorization: Bearer {token}" +``` + +**Response:** + +```json +{ + "message": "DSL deleted successfully" +} +``` + +### Load Management + +#### Load DSL + +Load a DSL resource into memory. + +``` +POST /load/{type} +``` + +**Request Body:** + +```json +{ + "id": "user", + "source": "{ ... }", + "store": "file" +} +``` + +**Response:** + +```json +{ + "message": "DSL loaded successfully" +} +``` + +#### Unload DSL + +Unload a DSL resource from memory. + +``` +POST /unload/{type} +``` + +**Request Body:** + +```json +{ + "id": "user", + "store": "file" +} +``` + +**Response:** + +```json +{ + "message": "DSL unloaded successfully" +} +``` + +#### Reload DSL + +Reload a DSL resource (unload then load). + +``` +POST /reload/{type} +``` + +**Request Body:** + +```json +{ + "id": "user", + "source": "{ ... }", + "store": "file" +} +``` + +**Response:** + +```json +{ + "message": "DSL reloaded successfully" +} +``` + +### Execution and Validation + +#### Execute DSL Method + +Execute a method on a loaded DSL resource. + +``` +POST /execute/{type}/{id}/{method} +``` + +**Request Body:** + +```json +{ + "args": ["arg1", "arg2"] +} +``` + +**Example:** + +```bash +curl -X POST "/v1/dsl/execute/model/user/find" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "args": [1, {"select": ["id", "name"]}] + }' +``` + +**Response:** + +```json +{ + "result": { + "id": 1, + "name": "John Doe" + } +} +``` + +#### Validate DSL Source + +Validate DSL source code syntax. + +``` +POST /validate/{type} +``` + +**Request Body:** + +```json +{ + "source": "{\n \"name\": \"user\",\n \"table\": {\n \"name\": \"users\"\n }\n}" +} +``` + +**Response:** + +```json +{ + "valid": true, + "messages": [] +} +``` + +Or if there are validation errors: + +```json +{ + "valid": false, + "messages": [ + { + "file": "", + "line": 5, + "column": 10, + "message": "Missing required field 'columns'", + "severity": "error" + } + ] +} +``` + +## Error Responses + +All endpoints return appropriate HTTP status codes and error messages: + +```json +{ + "error": "DSL ID is required" +} +``` + +Common HTTP status codes: + +- `200` - Success +- `201` - Created +- `400` - Bad Request (invalid parameters) +- `404` - Not Found +- `500` - Internal Server Error + +## Example Workflows + +### Creating a New Model + +1. **Validate the source first:** + +```bash +curl -X POST "/v1/dsl/validate/model" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "source": "{ \"name\": \"product\", \"table\": { \"name\": \"products\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }] }" + }' +``` + +2. **Create the model:** + +```bash +curl -X POST "/v1/dsl/create/model" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "product", + "source": "{ \"name\": \"product\", \"table\": { \"name\": \"products\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }] }", + "store": "file" + }' +``` + +3. **Verify it was created:** + +```bash +curl -X GET "/v1/dsl/inspect/model/product" \ + -H "Authorization: Bearer {token}" +``` + +### Updating and Reloading a DSL + +1. **Update the DSL:** + +```bash +curl -X PUT "/v1/dsl/update/model" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "product", + "source": "{ \"name\": \"product\", \"label\": \"Product Model\", \"table\": { \"name\": \"products\" }, \"columns\": [{ \"name\": \"id\", \"type\": \"ID\" }, { \"name\": \"name\", \"type\": \"string\" }] }" + }' +``` + +2. **Reload to apply changes:** + +```bash +curl -X POST "/v1/dsl/reload/model" \ + -H "Authorization: Bearer {token}" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "product", + "store": "file" + }' +``` + +This API provides comprehensive DSL management capabilities that align with the test cases and interface definitions in the codebase. diff --git a/openapi/dsl/dsl.go b/openapi/dsl/dsl.go new file mode 100644 index 00000000..501c6a2f --- /dev/null +++ b/openapi/dsl/dsl.go @@ -0,0 +1,442 @@ +package dsl + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/dsl" + "github.com/yaoapp/yao/dsl/types" + oauthTypes "github.com/yaoapp/yao/openapi/oauth/types" +) + +// Yao DSL Manager API + +// Attach attaches the DSL management handlers to the router +func Attach(group *gin.RouterGroup, oauth oauthTypes.OAuth) { + + // Protect all endpoints with OAuth + group.Handlers = append(group.Handlers, oauth.Guard) + + // DSL Information endpoints + group.GET("/inspect/:type/:id", inspect) + group.GET("/source/:type/:id", source) + group.GET("/path/:type/:id", path) + group.GET("/list/:type", list) + group.GET("/exists/:type/:id", exists) + + // DSL CRUD operations + group.POST("/create/:type", create) + group.PUT("/update/:type", update) + group.DELETE("/delete/:type/:id", delete) + + // DSL Load management + group.POST("/load/:type", load) + group.POST("/unload/:type", unload) + group.POST("/reload/:type", reload) + + // DSL Execute and Validate + group.POST("/execute/:type/:id/:method", execute) + group.POST("/validate/:type", validate) +} + +// Inspect DSL information +func inspect(c *gin.Context) { + dslType := types.Type(c.Param("type")) + id := c.Param("id") + + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + info, err := dslManager.Inspect(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, info) +} + +// Get DSL source code +func source(c *gin.Context) { + dslType := types.Type(c.Param("type")) + id := c.Param("id") + + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + sourceCode, err := dslManager.Source(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"source": sourceCode}) +} + +// Get DSL file path +func path(c *gin.Context) { + dslType := types.Type(c.Param("type")) + id := c.Param("id") + + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + filePath, err := dslManager.Path(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"path": filePath}) +} + +// List DSLs with optional filters +func list(c *gin.Context) { + dslType := types.Type(c.Param("type")) + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + // Parse query parameters + opts := &types.ListOptions{ + Sort: c.Query("sort"), + Order: c.Query("order"), + Store: types.StoreType(c.Query("store")), + Pattern: c.Query("pattern"), + } + + // Parse source flag + if sourceStr := c.Query("source"); sourceStr != "" { + if sourceBool, err := strconv.ParseBool(sourceStr); err == nil { + opts.Source = sourceBool + } + } + + // Parse tags from query parameter (comma-separated) + if tagsStr := c.Query("tags"); tagsStr != "" { + c.ShouldBindQuery(&struct { + Tags []string `form:"tags"` + }{Tags: opts.Tags}) + } + + infos, err := dslManager.List(c.Request.Context(), opts) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, infos) +} + +// Check if DSL exists +func exists(c *gin.Context) { + dslType := types.Type(c.Param("type")) + id := c.Param("id") + + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + exist, err := dslManager.Exists(c.Request.Context(), id) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"exists": exist}) +} + +// Create a new DSL +func create(c *gin.Context) { + dslType := types.Type(c.Param("type")) + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + var options types.CreateOptions + if err := c.ShouldBindJSON(&options); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()}) + return + } + + if options.ID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + if options.Source == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL source is required"}) + return + } + + err = dslManager.Create(c.Request.Context(), &options) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusCreated, gin.H{"message": "DSL created successfully"}) +} + +// Update an existing DSL +func update(c *gin.Context) { + dslType := types.Type(c.Param("type")) + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + var options types.UpdateOptions + if err := c.ShouldBindJSON(&options); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()}) + return + } + + if options.ID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + if options.Source == "" && options.Info == nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL source or info is required"}) + return + } + + err = dslManager.Update(c.Request.Context(), &options) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "DSL updated successfully"}) +} + +// Delete a DSL +func delete(c *gin.Context) { + dslType := types.Type(c.Param("type")) + id := c.Param("id") + + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + // Parse optional request body for delete options + var options types.DeleteOptions + options.ID = id + + // Try to bind JSON body if provided + c.ShouldBindJSON(&options) + + err = dslManager.Delete(c.Request.Context(), &options) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "DSL deleted successfully"}) +} + +// Load a DSL +func load(c *gin.Context) { + dslType := types.Type(c.Param("type")) + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + var options types.LoadOptions + if err := c.ShouldBindJSON(&options); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()}) + return + } + + if options.ID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + err = dslManager.Load(c.Request.Context(), &options) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "DSL loaded successfully"}) +} + +// Unload a DSL +func unload(c *gin.Context) { + dslType := types.Type(c.Param("type")) + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + var options types.UnloadOptions + if err := c.ShouldBindJSON(&options); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()}) + return + } + + if options.ID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + err = dslManager.Unload(c.Request.Context(), &options) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "DSL unloaded successfully"}) +} + +// Reload a DSL +func reload(c *gin.Context) { + dslType := types.Type(c.Param("type")) + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + var options types.ReloadOptions + if err := c.ShouldBindJSON(&options); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body: " + err.Error()}) + return + } + + if options.ID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + err = dslManager.Reload(c.Request.Context(), &options) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "DSL reloaded successfully"}) +} + +// Execute a DSL method +func execute(c *gin.Context) { + dslType := types.Type(c.Param("type")) + id := c.Param("id") + method := c.Param("method") + + if id == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL ID is required"}) + return + } + + if method == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "Method name is required"}) + return + } + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + // Parse arguments from request body + var requestBody struct { + Args []interface{} `json:"args"` + } + + if err := c.ShouldBindJSON(&requestBody); err != nil { + // If no body provided, execute without arguments + requestBody.Args = []interface{}{} + } + + result, err := dslManager.Execute(c.Request.Context(), id, method, requestBody.Args...) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"result": result}) +} + +// Validate DSL source code +func validate(c *gin.Context) { + dslType := types.Type(c.Param("type")) + + dslManager, err := dsl.New(dslType) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid DSL type: " + string(dslType)}) + return + } + + var requestBody struct { + Source string `json:"source" binding:"required"` + } + + if err := c.ShouldBindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "DSL source is required"}) + return + } + + valid, messages := dslManager.Validate(c.Request.Context(), requestBody.Source) + + c.JSON(http.StatusOK, gin.H{ + "valid": valid, + "messages": messages, + }) +} diff --git a/openapi/dsl_test.go b/openapi/dsl_test.go new file mode 100644 index 00000000..4e6c9b94 --- /dev/null +++ b/openapi/dsl_test.go @@ -0,0 +1,712 @@ +package openapi + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/dsl/types" +) + +// TestDSLCreate tests the DSL creation endpoint +func TestDSLCreate(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + // Get base URL from server config + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + // Register test client and get token + client := RegisterTestClient(t, "DSL Create Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // Generate unique test ID + testID := fmt.Sprintf("test_model_%d", time.Now().UnixNano()) + + // Model test data + modelSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Test User Model" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "name", "type": "string", "length": 80, "comment": "User Name", "index": true }, + { "name": "status", "type": "enum", "option": ["active", "disabled"], "default": "active", "comment": "Status", "index": true } + ], + "tags": ["test_%s"], + "label": "Test Model", + "description": "Test Model Description", + "option": { "timestamps": true, "soft_deletes": true } +}`, testID, testID, testID) + + // Test creation with different stores + stores := []string{"db", "file"} + + for _, store := range stores { + t.Run(fmt.Sprintf("CreateModel_%s", store), func(t *testing.T) { + // Prepare request body + createData := map[string]interface{}{ + "id": testID + "_" + store, + "source": modelSource, + "store": store, + } + + body, err := json.Marshal(createData) + assert.NoError(t, err) + + // Create HTTP request + req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + // Make request + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + // Check response + assert.Equal(t, http.StatusCreated, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, "DSL created successfully", response["message"]) + + t.Logf("Successfully created model DSL with store: %s", store) + }) + } +} + +// TestDSLInspect tests the DSL inspection endpoint +func TestDSLInspect(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + client := RegisterTestClient(t, "DSL Inspect Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + testID := fmt.Sprintf("test_inspect_%d", time.Now().UnixNano()) + modelSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Test Inspect Model" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "name", "type": "string", "length": 80, "comment": "User Name", "index": true } + ], + "tags": ["test_inspect"], + "label": "Test Inspect Model", + "description": "Test Model for Inspection", + "option": { "timestamps": true } +}`, testID, testID) + + // First create a model + createData := map[string]interface{}{ + "id": testID, + "source": modelSource, + "store": "db", + } + + body, _ := json.Marshal(createData) + req, _ := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + createResp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer createResp.Body.Close() + assert.Equal(t, http.StatusCreated, createResp.StatusCode) + + // Now test inspection + req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/inspect/model/"+testID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var info types.Info + err = json.NewDecoder(resp.Body).Decode(&info) + assert.NoError(t, err) + + // Verify the inspection results + assert.Equal(t, testID, info.ID) + assert.Equal(t, types.TypeModel, info.Type) + assert.Equal(t, "Test Inspect Model", info.Label) + assert.Equal(t, "Test Model for Inspection", info.Description) + assert.Contains(t, info.Tags, "test_inspect") + assert.False(t, info.Readonly) + assert.False(t, info.Builtin) + + t.Logf("Successfully inspected model DSL: %+v", info) +} + +// TestDSLSource tests the DSL source retrieval endpoint +func TestDSLSource(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + client := RegisterTestClient(t, "DSL Source Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + testID := fmt.Sprintf("test_source_%d", time.Now().UnixNano()) + modelSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Test Source Model" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "email", "type": "string", "length": 100, "comment": "Email", "index": true } + ], + "tags": ["test_source"], + "label": "Test Source Model", + "description": "Test Model for Source Retrieval" +}`, testID, testID) + + // Create model first + createData := map[string]interface{}{ + "id": testID, + "source": modelSource, + "store": "db", + } + + body, err := json.Marshal(createData) + assert.NoError(t, err) + req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + createResp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, createResp) + defer createResp.Body.Close() + + // Test source retrieval + req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/source/model/"+testID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + sourceReturned := response["source"].(string) + assert.Equal(t, modelSource, sourceReturned) + + t.Logf("Successfully retrieved model DSL source") +} + +// TestDSLList tests the DSL listing endpoint +func TestDSLList(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + client := RegisterTestClient(t, "DSL List Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + // Create multiple test models + testTag := fmt.Sprintf("test_list_%d", time.Now().UnixNano()) + + for i := 0; i < 3; i++ { + testID := fmt.Sprintf("test_list_model_%d_%d", time.Now().UnixNano(), i) + modelSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Test List Model %d" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "title", "type": "string", "length": 100 } + ], + "tags": ["%s"], + "label": "Test List Model %d" +}`, testID, testID, i, testTag, i) + + createData := map[string]interface{}{ + "id": testID, + "source": modelSource, + "store": "db", + } + + body, err := json.Marshal(createData) + assert.NoError(t, err) + req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + createResp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, createResp) + createResp.Body.Close() + } + + // Test listing all models + req, err := http.NewRequest("GET", serverURL+baseURL+"/dsl/list/model", nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var data []interface{} + err = json.NewDecoder(resp.Body).Decode(&data) + assert.NoError(t, err) + + assert.GreaterOrEqual(t, len(data), 3, "Should have at least 3 models") + + // Test listing with tags filter + req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/list/model?tags="+testTag, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var taggedData []interface{} + err = json.NewDecoder(resp.Body).Decode(&taggedData) + assert.NoError(t, err) + + assert.GreaterOrEqual(t, len(taggedData), 3, "Should find the tagged models") + + t.Logf("Successfully listed %d model DSLs", len(data)) +} + +// TestDSLUpdate tests the DSL update endpoint +func TestDSLUpdate(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + client := RegisterTestClient(t, "DSL Update Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + testID := fmt.Sprintf("test_update_%d", time.Now().UnixNano()) + + // Original model + originalSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Original Model" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "name", "type": "string", "length": 80 } + ], + "tags": ["test_update"], + "label": "Original Model" +}`, testID, testID) + + // Updated model + updatedSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Updated Model" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "name", "type": "string", "length": 80 }, + { "name": "email", "type": "string", "length": 100 } + ], + "tags": ["test_update", "updated"], + "label": "Updated Model", + "description": "Updated model description" +}`, testID, testID) + + // Create original model + createData := map[string]interface{}{ + "id": testID, + "source": originalSource, + "store": "db", + } + + body, err := json.Marshal(createData) + assert.NoError(t, err) + req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + createResp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, createResp) + createResp.Body.Close() + + // Update model + updateData := map[string]interface{}{ + "id": testID, + "source": updatedSource, + } + + body, err = json.Marshal(updateData) + assert.NoError(t, err) + req, err = http.NewRequest("PUT", serverURL+baseURL+"/dsl/update/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, "DSL updated successfully", response["message"]) + + // Verify the update by inspecting the model + req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/inspect/model/"+testID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + inspectResp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, inspectResp) + defer inspectResp.Body.Close() + + var info types.Info + err = json.NewDecoder(inspectResp.Body).Decode(&info) + assert.NoError(t, err) + + assert.Equal(t, "Updated Model", info.Label) + assert.Equal(t, "Updated model description", info.Description) + assert.Contains(t, info.Tags, "updated") + + t.Logf("Successfully updated model DSL") +} + +// TestDSLExists tests the DSL existence check endpoint +func TestDSLExists(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + client := RegisterTestClient(t, "DSL Exists Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + testID := fmt.Sprintf("test_exists_%d", time.Now().UnixNano()) + nonExistentID := fmt.Sprintf("non_existent_%d", time.Now().UnixNano()) + + // Test non-existent model first + req, err := http.NewRequest("GET", serverURL+baseURL+"/dsl/exists/model/"+nonExistentID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + assert.False(t, response["exists"].(bool)) + + // Create a model + modelSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s" }, + "columns": [{"name": "id", "type": "ID"}] +}`, testID, testID) + + createData := map[string]interface{}{ + "id": testID, + "source": modelSource, + "store": "db", + } + + body, err := json.Marshal(createData) + assert.NoError(t, err) + createReq, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + createReq.Header.Set("Content-Type", "application/json") + createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + createResp, err := http.DefaultClient.Do(createReq) + assert.NoError(t, err) + assert.NotNil(t, createResp) + createResp.Body.Close() + + // Test existing model + req, err = http.NewRequest("GET", serverURL+baseURL+"/dsl/exists/model/"+testID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err = http.DefaultClient.Do(req) + assert.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + assert.True(t, response["exists"].(bool)) + + t.Logf("Successfully tested model DSL existence") +} + +// TestDSLDelete tests the DSL deletion endpoint +func TestDSLDelete(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + client := RegisterTestClient(t, "DSL Delete Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + testID := fmt.Sprintf("test_delete_%d", time.Now().UnixNano()) + + // Create a model first + modelSource := fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s" }, + "columns": [{"name": "id", "type": "ID"}] +}`, testID, testID) + + createData := map[string]interface{}{ + "id": testID, + "source": modelSource, + "store": "db", + } + + body, err := json.Marshal(createData) + assert.NoError(t, err) + createReq, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/create/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + createReq.Header.Set("Content-Type", "application/json") + createReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + createResp, err := http.DefaultClient.Do(createReq) + assert.NoError(t, err) + assert.NotNil(t, createResp) + createResp.Body.Close() + + // Delete the model + req, err := http.NewRequest("DELETE", serverURL+baseURL+"/dsl/delete/model/"+testID, nil) + assert.NoError(t, err) + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, "DSL deleted successfully", response["message"]) + + // Verify deletion by checking existence + existsReq, err := http.NewRequest("GET", serverURL+baseURL+"/dsl/exists/model/"+testID, nil) + assert.NoError(t, err) + existsReq.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + existsResp, err := http.DefaultClient.Do(existsReq) + assert.NoError(t, err) + assert.NotNil(t, existsResp) + defer existsResp.Body.Close() + + var existsResponse map[string]interface{} + err = json.NewDecoder(existsResp.Body).Decode(&existsResponse) + assert.NoError(t, err) + assert.False(t, existsResponse["exists"].(bool)) + + t.Logf("Successfully deleted model DSL") +} + +// TestDSLValidate tests the DSL validation endpoint +func TestDSLValidate(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + client := RegisterTestClient(t, "DSL Validate Test Client", []string{"https://localhost/callback"}) + defer CleanupTestClient(t, client.ClientID) + tokenInfo := ObtainAccessToken(t, serverURL, client.ClientID, client.ClientSecret, "https://localhost/callback", "openid profile") + + tests := []struct { + name string + source string + description string + }{ + { + name: "ValidModel", + source: `{ + "name": "valid_model", + "table": { "name": "valid_model" }, + "columns": [ + {"name": "id", "type": "ID"}, + {"name": "name", "type": "string", "length": 80} + ] +}`, + description: "Valid model definition", + }, + { + name: "AnotherModel", + source: `{ + "name": "another_model", + "table": { "name": "another_model" }, + "columns": [ + {"name": "id", "type": "ID"}, + {"name": "title", "type": "string", "length": 100} + ] +}`, + description: "Another model definition", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + requestBody := map[string]string{ + "source": tt.source, + } + + body, err := json.Marshal(requestBody) + assert.NoError(t, err) + + req, err := http.NewRequest("POST", serverURL+baseURL+"/dsl/validate/model", bytes.NewBuffer(body)) + assert.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+tokenInfo.AccessToken) + + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var response map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&response) + assert.NoError(t, err) + + // Just verify the response has the expected structure + assert.Contains(t, response, "valid") + assert.Contains(t, response, "messages") + + valid, ok := response["valid"].(bool) + assert.True(t, ok, "valid should be a boolean") + + if messages, ok := response["messages"]; ok { + t.Logf("Validation messages for %s: %v", tt.description, messages) + } + + t.Logf("Successfully validated %s: valid=%v", tt.description, valid) + }) + } +} + +// TestDSLUnauthorized tests that endpoints return 401 when not authenticated +func TestDSLUnauthorized(t *testing.T) { + serverURL := Prepare(t) + defer Clean() + + baseURL := "" + if Server != nil && Server.Config != nil { + baseURL = Server.Config.BaseURL + } + + endpoints := []struct { + method string + path string + body string + }{ + {"GET", "/dsl/inspect/model/test", ""}, + {"GET", "/dsl/source/model/test", ""}, + {"GET", "/dsl/list/model", ""}, + {"GET", "/dsl/exists/model/test", ""}, + {"POST", "/dsl/create/model", `{"id":"test","source":"{}"}`}, + {"PUT", "/dsl/update/model", `{"id":"test","source":"{}"}`}, + {"DELETE", "/dsl/delete/model/test", ""}, + {"POST", "/dsl/validate/model", `{"source":"{}"}`}, + } + + for _, endpoint := range endpoints { + t.Run(fmt.Sprintf("Unauthorized_%s_%s", endpoint.method, endpoint.path), func(t *testing.T) { + var req *http.Request + var err error + + if endpoint.body != "" { + req, err = http.NewRequest(endpoint.method, serverURL+baseURL+endpoint.path, bytes.NewBufferString(endpoint.body)) + req.Header.Set("Content-Type", "application/json") + } else { + req, err = http.NewRequest(endpoint.method, serverURL+baseURL+endpoint.path, nil) + } + assert.NoError(t, err) + + // No Authorization header + resp, err := http.DefaultClient.Do(req) + assert.NoError(t, err) + assert.NotNil(t, resp) + defer resp.Body.Close() + + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) + + t.Logf("Correctly rejected unauthorized request to %s %s", endpoint.method, endpoint.path) + }) + } +} diff --git a/openapi/hello/README.md b/openapi/hello/README.md new file mode 100644 index 00000000..1fdc184b --- /dev/null +++ b/openapi/hello/README.md @@ -0,0 +1,270 @@ +# Hello World API + +Simple endpoints for testing connectivity, server status, and OAuth authentication functionality. + +## Base URL + +All endpoints are prefixed with the configured base URL followed by `/helloworld` (e.g., `/v1/helloworld`). + +## Endpoints + +### Public Endpoint + +Test basic server connectivity without authentication. + +``` +GET /helloworld/public +POST /helloworld/public +``` + +**No authentication required.** + +**Example:** + +```bash +curl -X GET "/v1/helloworld/public" +``` + +**Response:** + +```json +{ + "MESSAGE": "HELLO, WORLD", + "SERVER_TIME": "2024-01-15T10:30:00Z", + "VERSION": "1.0.0", + "PRVERSION": "1.0.0-preview", + "CUI": "1.0.0", + "PRCUI": "1.0.0-preview", + "APP": "YaoApp", + "APP_VERSION": "1.0.0" +} +``` + +**Response Fields:** + +- `MESSAGE` - Static "HELLO, WORLD" message +- `SERVER_TIME` - Current server time in RFC3339 format +- `VERSION` - Yao framework version +- `PRVERSION` - Yao framework preview version +- `CUI` - Yao CUI version +- `PRCUI` - Yao CUI preview version +- `APP` - Application name +- `APP_VERSION` - Application version + +### Protected Endpoint + +Test OAuth authentication and authorization. + +``` +GET /helloworld/protected +POST /helloworld/protected +``` + +**Authentication:** Required (OAuth Bearer token) + +**Headers:** + +``` +Authorization: Bearer {access_token} +``` + +**Example:** + +```bash +# First, obtain an access token +curl -X POST "/v1/oauth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&client_id=your_client_id&client_secret=your_client_secret" + +# Then use the token to access the protected endpoint +curl -X GET "/v1/helloworld/protected" \ + -H "Authorization: Bearer eyJhbGciOiJSUzI1NiIs..." +``` + +**Response:** + +Same response format as the public endpoint, confirming that authentication is working correctly. + +```json +{ + "MESSAGE": "HELLO, WORLD", + "SERVER_TIME": "2024-01-15T10:30:00Z", + "VERSION": "1.0.0", + "PRVERSION": "1.0.0-preview", + "CUI": "1.0.0", + "PRCUI": "1.0.0-preview", + "APP": "YaoApp", + "APP_VERSION": "1.0.0" +} +``` + +## HTTP Methods + +Both endpoints support both GET and POST methods, allowing flexibility for different client requirements and testing scenarios. + +## Error Responses + +### Unauthorized Access + +When accessing the protected endpoint without proper authentication: + +**HTTP Status:** `401 Unauthorized` + +**Response:** + +```json +{ + "error": "invalid_token", + "error_description": "The access token provided is expired, revoked, malformed, or invalid" +} +``` + +### Invalid Token + +When using an invalid or expired token: + +**HTTP Status:** `401 Unauthorized` + +**Response:** + +```json +{ + "error": "invalid_token", + "error_description": "The access token provided is invalid" +} +``` + +## Use Cases + +### Health Check + +Use the public endpoint as a health check for monitoring systems: + +```bash +# Simple health check +curl -f "/v1/helloworld/public" > /dev/null 2>&1 && echo "Service is healthy" || echo "Service is down" +``` + +### Authentication Testing + +Use the protected endpoint to verify OAuth authentication setup: + +```bash +# Test authentication flow +TOKEN=$(curl -s -X POST "/v1/oauth/token" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET" \ + | jq -r '.access_token') + +curl -X GET "/v1/helloworld/protected" \ + -H "Authorization: Bearer $TOKEN" +``` + +### Development and Debugging + +These endpoints are useful for: + +- **API Gateway Testing** - Verify routing and load balancer configuration +- **Authentication Debugging** - Test OAuth token validation +- **Environment Verification** - Check server version and configuration +- **Network Connectivity** - Basic reachability testing +- **Performance Baseline** - Minimal response time measurement + +## Integration Examples + +### JavaScript/Browser + +```javascript +// Public endpoint +fetch("/v1/helloworld/public") + .then((response) => response.json()) + .then((data) => console.log(data)); + +// Protected endpoint +const token = localStorage.getItem("access_token"); +fetch("/v1/helloworld/protected", { + headers: { + Authorization: `Bearer ${token}`, + }, +}) + .then((response) => response.json()) + .then((data) => console.log(data)); +``` + +### Python + +```python +import requests + +# Public endpoint +response = requests.get('/v1/helloworld/public') +print(response.json()) + +# Protected endpoint +headers = {'Authorization': f'Bearer {access_token}'} +response = requests.get('/v1/helloworld/protected', headers=headers) +print(response.json()) +``` + +### Go + +```go +package main + +import ( + "fmt" + "io" + "net/http" +) + +func testPublicEndpoint() { + resp, err := http.Get("/v1/helloworld/public") + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + fmt.Println(string(body)) +} + +func testProtectedEndpoint(token string) { + req, _ := http.NewRequest("GET", "/v1/helloworld/protected", nil) + req.Header.Set("Authorization", "Bearer "+token) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(resp.Body) + fmt.Println(string(body)) +} +``` + +## Security Considerations + +### Public Endpoint + +- No sensitive information is exposed +- Safe for use in monitoring and health checks +- Should be rate-limited to prevent abuse + +### Protected Endpoint + +- Requires valid OAuth access token +- Validates token signature and expiration +- Can be used to test authorization scopes (if implemented) + +## Server Information + +The response includes various version and application information useful for: + +- **Version Compatibility** - Ensure client compatibility with server version +- **Environment Identification** - Distinguish between development, staging, and production +- **Debugging** - Identify exact server version when reporting issues +- **Monitoring** - Track server deployments and version rollouts + +This API serves as a foundation for testing and validating the Yao OpenAPI infrastructure. diff --git a/openapi/hello.go b/openapi/hello/hello.go similarity index 58% rename from openapi/hello.go rename to openapi/hello/hello.go index b267587a..aca18b92 100644 --- a/openapi/hello.go +++ b/openapi/hello/hello.go @@ -1,30 +1,27 @@ -package openapi +package hello import ( "net/http" "time" "github.com/gin-gonic/gin" + "github.com/yaoapp/yao/openapi/oauth/types" "github.com/yaoapp/yao/share" ) -// attachHelloWorld attaches the hello world handlers to the router -func (openapi *OpenAPI) attachHelloWorld(base *gin.RouterGroup) { - - // hello handlers - hello := base.Group("/helloworld") +// Attach attaches the hello world handlers to the router +func Attach(group *gin.RouterGroup, oauth types.OAuth) { // Health check - hello.GET("/public", openapi.helloWorldPublic) - hello.POST("/public", openapi.helloWorldPublic) + group.GET("/public", helloWorldPublic) + group.POST("/public", helloWorldPublic) // OAuth Protected Resource - hello.GET("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected) - hello.POST("/protected", openapi.OAuth.Guard, openapi.helloWorldProtected) + group.GET("/protected", oauth.Guard, helloWorldProtected) + group.POST("/protected", oauth.Guard, helloWorldProtected) } -// helloWorldPublic is the handler for the hello world endpoint -func (openapi *OpenAPI) helloWorldPublic(c *gin.Context) { +func helloWorldPublic(c *gin.Context) { serverTime := time.Now().Format(time.RFC3339) c.JSON(http.StatusOK, gin.H{ "MESSAGE": "HELLO, WORLD", @@ -39,7 +36,7 @@ func (openapi *OpenAPI) helloWorldPublic(c *gin.Context) { } // helloWorldHello is the handler for the hello world endpoint -func (openapi *OpenAPI) helloWorldProtected(c *gin.Context) { +func helloWorldProtected(c *gin.Context) { serverTime := time.Now().Format(time.RFC3339) c.JSON(http.StatusOK, gin.H{ "MESSAGE": "HELLO, WORLD", diff --git a/openapi/openapi.go b/openapi/openapi.go index 025abeef..e867fd0d 100644 --- a/openapi/openapi.go +++ b/openapi/openapi.go @@ -6,6 +6,8 @@ import ( "github.com/gin-gonic/gin" "github.com/yaoapp/gou/application" "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/openapi/dsl" + "github.com/yaoapp/yao/openapi/hello" "github.com/yaoapp/yao/openapi/oauth" "github.com/yaoapp/yao/openapi/oauth/types" ) @@ -71,7 +73,10 @@ func (openapi *OpenAPI) Attach(router *gin.Engine) { openapi.attachOAuth(group) // Hello World handlers - openapi.attachHelloWorld(group) + hello.Attach(group.Group("/helloworld"), openapi.OAuth) + + // DSL handlers + dsl.Attach(group.Group("/dsl"), openapi.OAuth) // Custom handlers (Defined by developer) }