diff --git a/docs/development/backend/api-pagination.mdx b/docs/development/backend/api-pagination.mdx index 1b5bb19..669d05e 100644 --- a/docs/development/backend/api-pagination.mdx +++ b/docs/development/backend/api-pagination.mdx @@ -18,47 +18,66 @@ DeployStack uses **offset-based pagination** with standardized query parameters ## Standard Pagination Parameters -### Query Parameters +### Query Parameters Schema All paginated endpoints should accept these standardized query parameters: ```typescript -const paginationQuerySchema = z.object({ - limit: z.string() - .regex(/^\d+$/, 'Limit must be a number') - .transform(Number) - .refine(n => n > 0 && n <= 100, 'Limit must be between 1 and 100') - .optional() - .default('20'), - offset: z.string() - .regex(/^\d+$/, 'Offset must be a number') - .transform(Number) - .refine(n => n >= 0, 'Offset must be non-negative') - .optional() - .default('0') -}); +const PAGINATION_QUERY_SCHEMA = { + type: 'object', + properties: { + limit: { + type: 'string', + pattern: '^\\d+$', + description: 'Maximum number of items to return (1-100, default: 20)' + }, + offset: { + type: 'string', + pattern: '^\\d+$', + description: 'Number of items to skip from the beginning (≥0, default: 0)' + } + }, + additionalProperties: false +} as const; ``` #### Parameter Details - **`limit`** (optional, default: 20) - - Type: String (converted to Number) + - Type: String (converted to Number in handler) - Range: 1-100 - Description: Maximum number of items to return - Validation: Must be a positive integer between 1 and 100 - **`offset`** (optional, default: 0) - - Type: String (converted to Number) + - Type: String (converted to Number in handler) - Range: ≥ 0 - Description: Number of items to skip from the beginning - Validation: Must be a non-negative integer -### Why String Parameters? +### Parameter Validation in Handlers -Query parameters are always strings in HTTP. We use Zod's `.transform(Number)` to: -1. **Validate Format**: Ensure the string contains only digits -2. **Type Safety**: Convert to number for internal use -3. **Error Handling**: Provide clear validation messages +Query parameters are always strings in HTTP. Convert and validate them in your route handlers: + +```typescript +// Parse and validate pagination parameters +function validatePaginationParams(query: any): { limit: number; offset: number } { + const limit = query.limit ? parseInt(query.limit, 10) : 20; + const offset = query.offset ? parseInt(query.offset, 10) : 0; + + // Validate limit + if (isNaN(limit) || limit < 1 || limit > 100) { + throw new Error('Limit must be between 1 and 100'); + } + + // Validate offset + if (isNaN(offset) || offset < 0) { + throw new Error('Offset must be non-negative'); + } + + return { limit, offset }; +} +``` ## Standard Response Format @@ -67,21 +86,48 @@ Query parameters are always strings in HTTP. We use Zod's `.transform(Number)` t All paginated endpoints should return responses in this format: ```typescript -const paginatedResponseSchema = z.object({ - success: z.boolean(), - data: z.object({ - // Your actual data array - [dataArrayName]: z.array(yourItemSchema), - - // Pagination metadata - pagination: z.object({ - total: z.number(), // Total number of items available - limit: z.number(), // Items per page (as requested) - offset: z.number(), // Current offset (as requested) - has_more: z.boolean() // Whether more items are available - }) - }) -}); +const PAGINATED_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean' }, + data: { + type: 'object', + properties: { + // Your actual data array (name varies by endpoint) + items: { + type: 'array', + items: { /* your item schema */ } + }, + + // Pagination metadata + pagination: { + type: 'object', + properties: { + total: { + type: 'number', + description: 'Total number of items available' + }, + limit: { + type: 'number', + description: 'Items per page (as requested)' + }, + offset: { + type: 'number', + description: 'Current offset (as requested)' + }, + has_more: { + type: 'boolean', + description: 'Whether more items are available' + } + }, + required: ['total', 'limit', 'offset', 'has_more'] + } + }, + required: ['items', 'pagination'] + } + }, + required: ['success', 'data'] +} as const; ``` ### Response Example @@ -120,43 +166,95 @@ const paginatedResponseSchema = z.object({ ### 1. Route Schema Definition ```typescript -import { z } from 'zod'; -import { createSchema } from 'zod-openapi'; - -// Query parameters (including pagination) -const querySchema = z.object({ - // Your filtering parameters - category: z.string().optional(), - status: z.enum(['active', 'inactive']).optional(), - - // Standard pagination parameters - limit: z.string() - .regex(/^\d+$/, 'Limit must be a number') - .transform(Number) - .refine(n => n > 0 && n <= 100, 'Limit must be between 1 and 100') - .optional() - .default('20'), - offset: z.string() - .regex(/^\d+$/, 'Offset must be a number') - .transform(Number) - .refine(n => n >= 0, 'Offset must be non-negative') - .optional() - .default('0') -}); +import { type FastifyInstance } from 'fastify'; + +// Query parameters schema (including pagination) +const QUERY_SCHEMA = { + type: 'object', + properties: { + // Your filtering parameters + category: { + type: 'string', + description: 'Filter by category' + }, + status: { + type: 'string', + enum: ['active', 'inactive'], + description: 'Filter by status' + }, + + // Standard pagination parameters + limit: { + type: 'string', + pattern: '^\\d+$', + description: 'Maximum number of items to return (1-100, default: 20)' + }, + offset: { + type: 'string', + pattern: '^\\d+$', + description: 'Number of items to skip (≥0, default: 0)' + } + }, + additionalProperties: false +} as const; // Response schema -const responseSchema = z.object({ - success: z.boolean(), - data: z.object({ - items: z.array(yourItemSchema), - pagination: z.object({ - total: z.number(), - limit: z.number(), - offset: z.number(), - has_more: z.boolean() - }) - }) -}); +const RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean' }, + data: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + // Your item schema here + type: 'object', + properties: { + id: { type: 'string' }, + name: { type: 'string' }, + // ... other properties + } + } + }, + pagination: { + type: 'object', + properties: { + total: { type: 'number' }, + limit: { type: 'number' }, + offset: { type: 'number' }, + has_more: { type: 'boolean' } + }, + required: ['total', 'limit', 'offset', 'has_more'] + } + }, + required: ['items', 'pagination'] + } + }, + required: ['success', 'data'] +} as const; + +// TypeScript interfaces +interface QueryParams { + category?: string; + status?: 'active' | 'inactive'; + limit?: string; + offset?: string; +} + +interface PaginatedResponse { + success: boolean; + data: { + items: Item[]; + pagination: { + total: number; + limit: number; + offset: number; + has_more: boolean; + }; + }; +} ``` ### 2. Route Handler Implementation @@ -168,18 +266,31 @@ export default async function listItems(server: FastifyInstance) { tags: ['Items'], summary: 'List items with pagination', description: 'Retrieve items with pagination support. Supports filtering and sorting.', - querystring: createSchema(querySchema), + + querystring: QUERY_SCHEMA, + response: { - 200: createSchema(responseSchema) + 200: RESPONSE_SCHEMA, + 400: { + type: 'object', + properties: { + success: { type: 'boolean', default: false }, + error: { type: 'string' } + } + } } } }, async (request, reply) => { try { // Parse and validate query parameters - const params = querySchema.parse(request.query); + const query = request.query as QueryParams; + const { limit, offset } = validatePaginationParams(query); - // Extract pagination parameters - const { limit, offset, ...filters } = params; + // Extract filter parameters + const filters = { + category: query.category, + status: query.status + }; // Get all items (with filtering applied) const allItems = await yourService.getItems(filters); @@ -197,7 +308,7 @@ export default async function listItems(server: FastifyInstance) { }, 'Items list completed'); // Return paginated response - return reply.send({ + const response: PaginatedResponse = { success: true, data: { items: paginatedItems, @@ -208,13 +319,19 @@ export default async function listItems(server: FastifyInstance) { has_more: offset + limit < total } } - }); + }; + + const jsonString = JSON.stringify(response); + return reply.status(200).type('application/json').send(jsonString); } catch (error) { server.log.error({ error }, 'Failed to list items'); - return reply.status(500).send({ + + const errorResponse = { success: false, - error: 'Failed to retrieve items' - }); + error: error instanceof Error ? error.message : 'Failed to retrieve items' + }; + const jsonString = JSON.stringify(errorResponse); + return reply.status(400).type('application/json').send(jsonString); } }); } @@ -227,7 +344,7 @@ For better performance with large datasets, implement pagination at the database ### Using Drizzle ORM ```typescript -import { desc, asc } from 'drizzle-orm'; +import { desc, asc, sql, eq } from 'drizzle-orm'; async getItemsPaginated( filters: ItemFilters, @@ -269,7 +386,7 @@ async getItemsPaginated( // In your route handler const { items, total } = await yourService.getItemsPaginated(filters, limit, offset); -return reply.send({ +const response: PaginatedResponse = { success: true, data: { items, @@ -280,7 +397,10 @@ return reply.send({ has_more: offset + limit < total } } -}); +}; + +const jsonString = JSON.stringify(response); +return reply.status(200).type('application/json').send(jsonString); ``` ## Client-Side Usage Examples @@ -396,47 +516,68 @@ export function usePagination( Always use the same validation rules across all endpoints: ```typescript -// Create a reusable schema -export const paginationSchema = z.object({ - limit: z.string() - .regex(/^\d+$/, 'Limit must be a number') - .transform(Number) - .refine(n => n > 0 && n <= 100, 'Limit must be between 1 and 100') - .optional() - .default('20'), - offset: z.string() - .regex(/^\d+$/, 'Offset must be a number') - .transform(Number) - .refine(n => n >= 0, 'Offset must be non-negative') - .optional() - .default('0') -}); +// Create a reusable validation function +export function validatePaginationParams(query: any): { limit: number; offset: number } { + const limit = query.limit ? parseInt(query.limit, 10) : 20; + const offset = query.offset ? parseInt(query.offset, 10) : 0; + + if (isNaN(limit) || limit < 1 || limit > 100) { + throw new Error('Limit must be between 1 and 100'); + } + + if (isNaN(offset) || offset < 0) { + throw new Error('Offset must be non-negative'); + } + + return { limit, offset }; +} + +// Reusable schema constant +export const PAGINATION_QUERY_SCHEMA = { + type: 'object', + properties: { + limit: { + type: 'string', + pattern: '^\\d+$', + description: 'Maximum number of items to return (1-100, default: 20)' + }, + offset: { + type: 'string', + pattern: '^\\d+$', + description: 'Number of items to skip (≥0, default: 0)' + } + }, + additionalProperties: false +} as const; // Use in your endpoint schemas -const querySchema = z.object({ - // Your specific filters - category: z.string().optional(), - status: z.enum(['active', 'inactive']).optional(), - - // Include pagination - ...paginationSchema.shape -}); +const QUERY_SCHEMA = { + type: 'object', + properties: { + // Your specific filters + category: { type: 'string' }, + status: { type: 'string', enum: ['active', 'inactive'] }, + + // Include pagination + ...PAGINATION_QUERY_SCHEMA.properties + }, + additionalProperties: false +} as const; ``` ### 2. Proper Error Handling ```typescript try { - const params = querySchema.parse(request.query); + const { limit, offset } = validatePaginationParams(request.query); + // ... rest of handler } catch (error) { - if (error instanceof z.ZodError) { - return reply.status(400).send({ - success: false, - error: 'Invalid query parameters', - details: error.errors - }); - } - throw error; + const errorResponse = { + success: false, + error: error instanceof Error ? error.message : 'Invalid query parameters' + }; + const jsonString = JSON.stringify(errorResponse); + return reply.status(400).type('application/json').send(jsonString); } ``` @@ -504,10 +645,9 @@ const limit = parseInt(request.query.limit) || 20; const offset = parseInt(request.query.offset) || 0; ``` -✅ **Correct**: Proper Zod validation +✅ **Correct**: Proper validation function ```typescript -const params = paginationSchema.parse(request.query); -const { limit, offset } = params; +const { limit, offset } = validatePaginationParams(request.query); ``` ### 3. Performance Issues @@ -553,13 +693,14 @@ export default async function listServers(server: FastifyInstance) { tags: ['MCP Servers'], summary: 'List MCP servers', description: 'Retrieve MCP servers with pagination support...', - querystring: createSchema(querySchema), + querystring: QUERY_SCHEMA, response: { - 200: createSchema(listServersResponseSchema) + 200: LIST_SERVERS_RESPONSE_SCHEMA } } }, async (request, reply) => { - const { limit, offset, ...filters } = querySchema.parse(request.query); + const { limit, offset } = validatePaginationParams(request.query); + const filters = extractFilters(request.query); const allServers = await catalogService.getServersForUser( userId, userRole, teamIds, filters @@ -568,7 +709,7 @@ export default async function listServers(server: FastifyInstance) { const total = allServers.length; const paginatedServers = allServers.slice(offset, offset + limit); - return reply.send({ + const response = { success: true, data: { servers: paginatedServers, @@ -579,7 +720,10 @@ export default async function listServers(server: FastifyInstance) { has_more: offset + limit < total } } - }); + }; + + const jsonString = JSON.stringify(response); + return reply.status(200).type('application/json').send(jsonString); }); } ``` diff --git a/docs/development/backend/api-security.mdx b/docs/development/backend/api-security.mdx index d7d1baa..05994b3 100644 --- a/docs/development/backend/api-security.mdx +++ b/docs/development/backend/api-security.mdx @@ -58,26 +58,103 @@ Understanding Fastify's hook execution order is essential for proper security im ```typescript import { requireGlobalAdmin } from '../../../middleware/roleMiddleware'; -export default async function secureRoute(fastify: FastifyInstance) { - fastify.post<{ Body: RequestInput }>('/protected-endpoint', { +// Reusable Schema Constants +const REQUEST_SCHEMA = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, description: 'Name is required' }, + value: { type: 'string', description: 'Value field' } + }, + required: ['name', 'value'], + additionalProperties: false +} as const; + +const SUCCESS_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean' }, + message: { type: 'string' } + }, + required: ['success', 'message'] +} as const; + +const ERROR_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean', default: false }, + error: { type: 'string' } + }, + required: ['success', 'error'] +} as const; + +// TypeScript interfaces +interface RequestBody { + name: string; + value: string; +} + +interface SuccessResponse { + success: boolean; + message: string; +} + +interface ErrorResponse { + success: boolean; + error: string; +} + +export default async function secureRoute(server: FastifyInstance) { + server.post('/protected-endpoint', { + preValidation: requireGlobalAdmin(), // ✅ CORRECT: Runs before validation schema: { tags: ['Protected'], summary: 'Protected endpoint', description: 'Requires admin permissions', security: [{ cookieAuth: [] }], - body: createSchema(RequestSchema), + + // Fastify validation schema + body: REQUEST_SCHEMA, + + // OpenAPI documentation (same schema, reused) + requestBody: { + required: true, + content: { + 'application/json': { + schema: REQUEST_SCHEMA + } + } + }, + response: { - 200: createSchema(SuccessResponseSchema.describe('Success')), - 401: createSchema(ErrorResponseSchema.describe('Unauthorized')), - 403: createSchema(ErrorResponseSchema.describe('Forbidden')), - 400: createSchema(ErrorResponseSchema.describe('Bad Request')) + 200: { + ...SUCCESS_RESPONSE_SCHEMA, + description: 'Success' + }, + 401: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Unauthorized' + }, + 403: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Forbidden' + }, + 400: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Bad Request' + } } - }, - preValidation: requireGlobalAdmin(), // ✅ CORRECT: Runs before validation, + } }, async (request, reply) => { // If we reach here, user is authorized AND input is validated - const validatedData = request.body; + const validatedData = request.body as RequestBody; + // Your business logic here + const successResponse: SuccessResponse = { + success: true, + message: 'Operation completed successfully' + }; + const jsonString = JSON.stringify(successResponse); + return reply.status(200).type('application/json').send(jsonString); }); } ``` @@ -85,14 +162,19 @@ export default async function secureRoute(fastify: FastifyInstance) { ### ❌ Insecure Pattern: preHandler for Authorization ```typescript -export default async function insecureRoute(fastify: FastifyInstance) { - fastify.post<{ Body: RequestInput }>('/protected-endpoint', { +export default async function insecureRoute(server: FastifyInstance) { + server.post('/protected-endpoint', { schema: { // Schema definition... - body: zodToJsonSchema(RequestSchema, { - $refStrategy: 'none', - target: 'openApi3' - }) + body: { + type: 'object', + properties: { + name: { type: 'string', minLength: 1 }, + value: { type: 'string' } + }, + required: ['name', 'value'], + additionalProperties: false + } }, preHandler: requireGlobalAdmin(), // ❌ WRONG: Runs after validation }, async (request, reply) => { @@ -161,22 +243,24 @@ For endpoints that support both web users (cookies) and CLI users (OAuth2 Bearer ```typescript import { requireAuthenticationAny, requireOAuthScope } from '../../middleware/oauthMiddleware'; -fastify.get('/dual-auth-endpoint', { - schema: { - security: [ - { cookieAuth: [] }, // Cookie authentication - { bearerAuth: [] } // OAuth2 Bearer token - ] - }, - preValidation: [ - requireAuthenticationAny(), // Accept either auth method - requireOAuthScope('your:scope') // Enforce OAuth2 scope - ] -}, async (request, reply) => { - // Endpoint accessible via both authentication methods - const authType = request.tokenPayload ? 'oauth2' : 'cookie'; - const userId = request.user!.id; -}); +export default async function dualAuthRoute(server: FastifyInstance) { + server.get('/dual-auth-endpoint', { + preValidation: [ + requireAuthenticationAny(), // Accept either auth method + requireOAuthScope('your:scope') // Enforce OAuth2 scope + ], + schema: { + security: [ + { cookieAuth: [] }, // Cookie authentication + { bearerAuth: [] } // OAuth2 Bearer token + ] + } + }, async (request, reply) => { + // Endpoint accessible via both authentication methods + const authType = request.tokenPayload ? 'oauth2' : 'cookie'; + const userId = request.user!.id; + }); +} ``` For detailed OAuth2 implementation, see the [Backend OAuth Implementation Guide](/development/backend/oauth-providers) and [Backend Security Policy](/development/backend/security#oauth2-server-security). @@ -188,31 +272,102 @@ For endpoints that operate within team contexts (e.g., `/teams/:teamId/resource` ```typescript import { requireTeamPermission } from '../../../middleware/roleMiddleware'; -export default async function teamResourceRoute(fastify: FastifyInstance) { - fastify.post<{ - Params: { teamId: string }; - Body: CreateResourceRequest; - }>('/teams/:teamId/resources', { +// Reusable Schema Constants +const CREATE_RESOURCE_SCHEMA = { + type: 'object', + properties: { + name: { type: 'string', minLength: 1, description: 'Name is required' }, + description: { type: 'string', description: 'Optional description' } + }, + required: ['name'], + additionalProperties: false +} as const; + +const SUCCESS_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean' }, + message: { type: 'string' } + }, + required: ['success', 'message'] +} as const; + +const ERROR_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean', default: false }, + error: { type: 'string' } + }, + required: ['success', 'error'] +} as const; + +// TypeScript interfaces +interface CreateResourceRequest { + name: string; + description?: string; +} + +interface SuccessResponse { + success: boolean; + message: string; +} + +interface ErrorResponse { + success: boolean; + error: string; +} + +export default async function teamResourceRoute(server: FastifyInstance) { + server.post('/teams/:teamId/resources', { + preValidation: requireTeamPermission('resources.create'), // ✅ Team-aware authorization schema: { tags: ['Team Resources'], summary: 'Create team resource', description: 'Creates a resource within the specified team context', security: [{ cookieAuth: [] }], - params: zodToJsonSchema(z.object({ - teamId: z.string().min(1, 'Team ID is required') - })), - body: zodToJsonSchema(CreateResourceSchema), + + params: { + type: 'object', + properties: { + teamId: { type: 'string', minLength: 1 } + }, + required: ['teamId'], + additionalProperties: false + }, + + body: CREATE_RESOURCE_SCHEMA, + + requestBody: { + required: true, + content: { + 'application/json': { + schema: CREATE_RESOURCE_SCHEMA + } + } + }, + response: { - 201: zodToJsonSchema(SuccessResponseSchema), - 401: zodToJsonSchema(ErrorResponseSchema.describe('Unauthorized')), - 403: zodToJsonSchema(ErrorResponseSchema.describe('Forbidden - Not team member or insufficient permissions')), - 400: zodToJsonSchema(ErrorResponseSchema.describe('Bad Request')) + 201: { + ...SUCCESS_RESPONSE_SCHEMA, + description: 'Resource created successfully' + }, + 401: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Unauthorized' + }, + 403: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Forbidden - Not team member or insufficient permissions' + }, + 400: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Bad Request' + } } - }, - preValidation: requireTeamPermission('resources.create'), // ✅ Team-aware authorization + } }, async (request, reply) => { - const { teamId } = request.params; - const resourceData = request.body; + const { teamId } = request.params as { teamId: string }; + const resourceData = request.body as CreateResourceRequest; // User is guaranteed to be: // 1. Authenticated @@ -221,6 +376,12 @@ export default async function teamResourceRoute(fastify: FastifyInstance) { // 4. Input is validated // Your business logic here + const successResponse: SuccessResponse = { + success: true, + message: `Resource "${resourceData.name}" created successfully` + }; + const jsonString = JSON.stringify(successResponse); + return reply.status(201).type('application/json').send(jsonString); }); } ``` @@ -358,21 +519,21 @@ team_user: [ ```typescript // Global admin only -fastify.delete('/admin/users/:id', { - schema: { /* ... */ }, +server.delete('/admin/users/:id', { preValidation: requireGlobalAdmin(), + schema: { /* ... */ } }, handler); // Specific permission required -fastify.post('/settings/bulk', { - schema: { /* ... */ }, +server.post('/settings/bulk', { preValidation: requirePermission('settings.edit'), + schema: { /* ... */ } }, handler); // User can access own data OR admin can access any -fastify.get('/users/:id/profile', { - schema: { /* ... */ }, +server.get('/users/:id/profile', { preValidation: requireOwnershipOrAdmin(getUserIdFromParams), + schema: { /* ... */ } }, handler); ``` @@ -441,13 +602,13 @@ For complex authorization requirements: ```typescript // Multiple checks in sequence -fastify.post('/complex-endpoint', { - schema: { /* ... */ }, +server.post('/complex-endpoint', { preValidation: [ requireAuthentication(), // Must be logged in requireRole('team_member'), // Must have team role requirePermission('data.write') // Must have write permission ], + schema: { /* ... */ } }, handler); ``` @@ -465,9 +626,9 @@ async function conditionalAuth(request: FastifyRequest, reply: FastifyReply) { } } -fastify.post('/conditional-endpoint', { - schema: { /* ... */ }, +server.post('/conditional-endpoint', { preValidation: conditionalAuth, + schema: { /* ... */ } }, handler); ``` diff --git a/docs/development/backend/api.mdx b/docs/development/backend/api.mdx index 14a6952..0c17983 100644 --- a/docs/development/backend/api.mdx +++ b/docs/development/backend/api.mdx @@ -9,11 +9,12 @@ This document explains how to generate and use the OpenAPI specification for the ## Overview -The DeployStack Backend uses Fastify with Swagger plugins to automatically generate OpenAPI 3.0 specifications. Route schemas are defined using [Zod](https://zod.dev/) for type safety and expressiveness, and then converted to JSON Schema using the [zod-openapi](https://www.npmjs.com/package/zod-openapi) library. This provides: +The DeployStack Backend uses Fastify with Swagger plugins to automatically generate OpenAPI 3.0 specifications. Route schemas are defined using **reusable JSON Schema constants** for type safety and documentation. This provides: - **Interactive Documentation**: Swagger UI interface for testing APIs - **Postman Integration**: JSON/YAML specs that can be imported into Postman - **Automated Generation**: Specifications are generated from actual route code +- **Type Safety**: TypeScript interfaces provide compile-time checking ## 🔒 Security First @@ -49,22 +50,24 @@ Use these middleware functions to enable dual authentication on endpoints: ```typescript import { requireAuthenticationAny, requireOAuthScope } from '../../middleware/oauthMiddleware'; -fastify.get('/your-endpoint', { - schema: { - security: [ - { cookieAuth: [] }, // Cookie authentication - { bearerAuth: [] } // OAuth2 Bearer token - ] - }, - preValidation: [ - requireAuthenticationAny(), // Accept either auth method - requireOAuthScope('your:scope') // Enforce OAuth2 scope - ] -}, async (request, reply) => { - // Endpoint accessible via both authentication methods - const authType = request.tokenPayload ? 'oauth2' : 'cookie'; - const userId = request.user!.id; -}); +export default async function yourRoute(server: FastifyInstance) { + server.get('/your-endpoint', { + preValidation: [ + requireAuthenticationAny(), // Accept either auth method + requireOAuthScope('your:scope') // Enforce OAuth2 scope + ], + schema: { + security: [ + { cookieAuth: [] }, // Cookie authentication + { bearerAuth: [] } // OAuth2 Bearer token + ] + } + }, async (request, reply) => { + // Endpoint accessible via both authentication methods + const authType = request.tokenPayload ? 'oauth2' : 'cookie'; + const userId = request.user!.id; + }); +} ``` ### OAuth2 Scopes @@ -242,37 +245,178 @@ services/backend/src/routes/mcp/ - **Clear Responsibility**: Each file has a single, clear purpose - **Reduced Complexity**: Avoid hundreds of lines in single files +### Shared Schemas for CRUD Modules (Mandatory) + +**MANDATORY PATTERN**: For route directories that implement complete CRUD operations on a single entity (Create, Read, Update, Delete), you **must** create a shared `schemas.ts` file to eliminate duplication and ensure consistency. + +#### When to Create Shared Schemas + +Create a `schemas.ts` file when your route directory contains: +- **Multiple endpoints** operating on the same core entity +- **Duplicate schema definitions** across route files +- **Common response structures** (error responses, entity objects) +- **Shared parameter validation** (ID parameters, common fields) + +#### What to Include in Shared Schemas + +**Always Share:** +- **Error response schemas** that appear in multiple files +- **Core entity object schemas** used in responses +- **Common parameter schemas** (ID validation, shared fields) +- **Shared TypeScript interfaces** for consistent typing + +**Keep Endpoint-Specific:** +- **Request body schemas** with different requirements (create vs. update) +- **Endpoint-specific success response wrappers** +- **Unique validation rules** specific to individual operations + +#### Implementation Requirements + +1. **File Naming**: Use `schemas.ts` in the route directory +2. **Export Pattern**: Export const schemas and TypeScript interfaces +3. **Import Pattern**: Import shared components into individual route files +4. **Documentation**: Include clear comments explaining shared vs. specific schemas + +#### Benefits of Shared Schemas + +- **Single Source of Truth**: Entity structure defined once +- **Elimination of Duplication**: Removes copy-pasted schema definitions +- **Improved Maintainability**: Changes happen in one place +- **Enhanced Consistency**: All endpoints use identical shared structures +- **Better Type Safety**: Shared interfaces prevent type drift + +#### Module Examples Requiring Shared Schemas + +- `/mcp/categories/` - Complete category management CRUD +- `/mcp/servers/` - Server management operations +- `/teams/` - Team management functionality +- Any directory with 3+ routes operating on the same entity + ### Route File Template -Each route file should follow this pattern: +Each route file should follow this **recommended pattern**: ```typescript import { type FastifyInstance } from 'fastify' -import { z } from 'zod' -import { createSchema } from 'zod-openapi' -// Define your schemas -const responseSchema = z.object({ - // Your response structure -}); +// Reusable Schema Constants +const REQUEST_SCHEMA = { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 1, + description: 'Name is required' + }, + email: { + type: 'string', + format: 'email', + description: 'Valid email required' + } + }, + required: ['name', 'email'], + additionalProperties: false +} as const; + +const SUCCESS_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean' }, + message: { type: 'string' } + }, + required: ['success', 'message'] +} as const; + +const ERROR_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { type: 'boolean', default: false }, + error: { type: 'string' } + }, + required: ['success', 'error'] +} as const; + +// TypeScript interfaces for type safety +interface RequestBody { + name: string; + email: string; +} + +interface SuccessResponse { + success: boolean; + message: string; +} + +interface ErrorResponse { + success: boolean; + error: string; +} export default async function yourRoute(server: FastifyInstance) { - server.get('/your-endpoint', { + server.post('/your-endpoint', { + preValidation: requirePermission('your.permission'), // Authorization FIRST schema: { tags: ['Your Category'], summary: 'Brief description', - description: 'Detailed description', + description: 'Detailed description. Requires Content-Type: application/json header when sending request body.', + security: [{ cookieAuth: [] }], + + // Fastify validation schema + body: REQUEST_SCHEMA, + + // OpenAPI documentation (same schema, reused) + requestBody: { + required: true, + content: { + 'application/json': { + schema: REQUEST_SCHEMA + } + } + }, + response: { - 200: createSchema(responseSchema) + 200: { + ...SUCCESS_RESPONSE_SCHEMA, + description: 'Success' + }, + 400: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Bad Request' + }, + 401: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Unauthorized' + }, + 403: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Forbidden' + } } } - }, async () => { + }, async (request, reply) => { + // TypeScript type assertion (Fastify has already validated) + const { name, email } = request.body as RequestBody; + // Your route logic - return { /* your response */ } + const successResponse: SuccessResponse = { + success: true, + message: `User ${name} processed successfully` + }; + const jsonString = JSON.stringify(successResponse); + return reply.status(200).type('application/json').send(jsonString); }); } ``` +**Key Requirements:** +- ✅ **Parameter name**: `server: FastifyInstance` (not `fastify`) +- ✅ **Method calls**: `server.post()` (not `fastify.post()`) +- ✅ **preValidation first**: Authorization before schema +- ✅ **Reusable schemas**: Define schema constants at the top +- ✅ **Single source**: Same schema for both `body` and `requestBody` +- ✅ **TypeScript interfaces**: Provide type safety +- ✅ **Manual JSON serialization**: Use `JSON.stringify()` for responses + ### Registration in index.ts Import and register your route in `src/routes/index.ts`: @@ -386,7 +530,7 @@ const routeSchema = { required: true, content: { 'application/json': { - schema: createSchema(requestSchema) + schema: REQUEST_SCHEMA } } }, @@ -396,118 +540,167 @@ const routeSchema = { ## Adding Documentation to Routes -To add OpenAPI documentation to your routes, define your request body and response schemas using Zod. Then, use the `createSchema` utility to convert these Zod schemas into the JSON Schema format expected by Fastify. - -Make sure you have `zod` and `zod-openapi` installed in your backend service. +The DeployStack Backend uses **reusable JSON Schema constants** for both validation and documentation generation. This approach provides a single source of truth for API schemas. -### Recommended Approach: Dual-Schema Pattern for Validation + Documentation - -**IMPORTANT**: After the Zod v4 migration, we use a **dual-schema approach** to ensure both proper Fastify validation and accurate OpenAPI documentation. +### Recommended Approach: Reusable Schema Constants ```typescript -import { z } from 'zod'; -import { createSchema } from 'zod-openapi'; - -// 1. Define your Zod schemas for request body, responses, etc. -const myRequestBodySchema = z.object({ - name: z.string().min(3).describe("The name of the item (min 3 chars)"), - count: z.number().positive().describe("How many items (must be positive)"), - type: z.enum(['mysql', 'sqlite']).describe("Database engine type") -}); - -const mySuccessResponseSchema = z.object({ - success: z.boolean().describe("Indicates if the operation was successful"), - itemId: z.string().uuid().describe("The UUID of the created/affected item"), - message: z.string().optional().describe("Optional success message") -}); - -const myErrorResponseSchema = z.object({ - success: z.boolean().default(false).describe("Indicates failure"), - error: z.string().describe("Error message detailing what went wrong") -}); - -// 2. Construct the Fastify route schema using DUAL-SCHEMA PATTERN -const routeSchema = { - tags: ['Category'], // Your API category - summary: 'Brief description of your endpoint', - description: 'Detailed description of what this endpoint does, its parameters, and expected outcomes. Requires Content-Type: application/json header when sending request body.', - security: [{ cookieAuth: [] }], // Include if authentication is required - - // ✅ CRITICAL: Use plain JSON Schema for Fastify validation - body: { - type: 'object', - properties: { - name: { type: 'string', minLength: 3 }, - count: { type: 'number', minimum: 1 }, - type: { type: 'string', enum: ['mysql', 'sqlite'] } +import { type FastifyInstance } from 'fastify'; + +// 1. Define reusable JSON Schema constants +const REQUEST_SCHEMA = { + type: 'object', + properties: { + name: { + type: 'string', + minLength: 3, + description: 'The name of the item (min 3 chars)' + }, + count: { + type: 'number', + minimum: 1, + description: 'How many items (must be positive)' }, - required: ['name', 'count', 'type'], - additionalProperties: false + type: { + type: 'string', + enum: ['mysql', 'sqlite'], + description: 'Database engine type' + } }, - - // ✅ Use createSchema() for OpenAPI documentation - requestBody: { - required: true, - content: { - 'application/json': { - schema: createSchema(myRequestBodySchema) - } + required: ['name', 'count', 'type'], + additionalProperties: false +} as const; + +const SUCCESS_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { + type: 'boolean', + description: 'Indicates if the operation was successful' + }, + itemId: { + type: 'string', + description: 'The UUID of the created/affected item' + }, + message: { + type: 'string', + description: 'Optional success message' } }, - response: { - 200: createSchema(mySuccessResponseSchema.describe("Successful operation")), - 400: createSchema(myErrorResponseSchema.describe("Bad Request - Invalid input")), - // Define other responses (e.g., 401, 403, 404, 500) similarly - } -}; + required: ['success', 'itemId'] +} as const; + +const ERROR_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { + type: 'boolean', + default: false, + description: 'Indicates failure' + }, + error: { + type: 'string', + description: 'Error message detailing what went wrong' + } + }, + required: ['success', 'error'] +} as const; -// 3. Use the schema in your Fastify route definition with proper TypeScript typing +// 2. Define TypeScript interfaces for type safety interface RequestBody { name: string; count: number; type: 'mysql' | 'sqlite'; } -fastify.post<{ Body: RequestBody }>( - '/your-route', - { schema: routeSchema }, - async (request, reply) => { - // ✅ Fastify has already validated request.body using the JSON schema - // ✅ If we reach here, request.body is guaranteed to be valid - // ✅ No manual validation needed! - - const { name, count, type } = request.body; // Fully typed and validated +interface SuccessResponse { + success: boolean; + itemId: string; + message?: string; +} + +interface ErrorResponse { + success: boolean; + error: string; +} + +// 3. Use schemas in route definition +export default async function yourRoute(server: FastifyInstance) { + server.post('/your-route', { + preValidation: requirePermission('your.permission'), + schema: { + tags: ['Category'], // Your API category + summary: 'Brief description of your endpoint', + description: 'Detailed description of what this endpoint does, its parameters, and expected outcomes. Requires Content-Type: application/json header when sending request body.', + security: [{ cookieAuth: [] }], // Include if authentication is required + + // Single schema for both validation AND documentation + body: REQUEST_SCHEMA, + + // OpenAPI documentation (same schema, reused) + requestBody: { + required: true, + content: { + 'application/json': { + schema: REQUEST_SCHEMA + } + } + }, + + response: { + 200: { + ...SUCCESS_RESPONSE_SCHEMA, + description: 'Successful operation' + }, + 400: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Bad Request - Invalid input' + }, + 401: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Unauthorized' + }, + 403: { + ...ERROR_RESPONSE_SCHEMA, + description: 'Forbidden' + } + } + } + }, async (request, reply) => { + // TypeScript type assertion (Fastify has already validated) + const { name, count, type } = request.body as RequestBody; // Your route handler logic here - const successResponse = { + const successResponse: SuccessResponse = { success: true, itemId: 'some-uuid-v4-here', message: `Item ${name} processed successfully with ${count} items using ${type}.` }; const jsonString = JSON.stringify(successResponse); return reply.status(200).type('application/json').send(jsonString); - } -); + }); +} ``` ### Key Benefits of This Approach -1. **Single Source of Truth**: Zod schemas define both validation AND documentation +1. **Single Source of Truth**: JSON Schema constants define both validation AND documentation 2. **Automatic Validation**: Fastify automatically validates requests before your handler runs -3. **No Manual Validation**: Remove all manual `zod.parse()` calls and field checks +3. **No Manual Validation**: Remove all manual validation calls and field checks 4. **Better Error Messages**: Fastify provides detailed validation errors automatically 5. **Type Safety**: Handlers receive properly typed, validated data 6. **Cleaner Code**: No redundant validation logic in handlers +7. **Schema Reuse**: Same schema serves both validation and documentation ## JSON Response Serialization Pattern -**CRITICAL**: After the Zod v4 migration, all API responses must use manual JSON serialization to prevent `"[object Object]"` serialization issues. +**CRITICAL**: All API responses must use manual JSON serialization to ensure consistent JSON output. ### Required Response Pattern ```typescript // ✅ CORRECT: Manual JSON serialization -const successResponse = { +const successResponse: SuccessResponse = { success: true, message: 'Operation completed successfully', data: { /* your data */ } @@ -519,10 +712,10 @@ return reply.status(200).type('application/json').send(jsonString); ### What NOT to Do ```typescript -// ❌ WRONG: Direct object response (causes serialization issues) +// ❌ WRONG: Direct object response (can cause serialization issues) return reply.status(200).send({ success: true, - message: 'This will become "[object Object]"' + message: 'This might not serialize correctly' }); // ❌ WRONG: Using reply.send() without JSON.stringify() @@ -536,7 +729,7 @@ All error responses must also use manual JSON serialization: ```typescript // ✅ CORRECT: Error response with manual serialization -const errorResponse = { +const errorResponse: ErrorResponse = { success: false, error: 'Detailed error message' }; @@ -560,22 +753,18 @@ return reply.status(401).type('application/json').send(jsonString); ### Why This Pattern is Required -After the Zod v4 migration, Fastify's automatic JSON serialization can fail with complex objects, resulting in: -- Response bodies showing `"[object Object]"` instead of actual data -- Client applications receiving unparseable responses -- Test failures due to missing `success` and `error` properties - The manual JSON serialization pattern ensures: - ✅ Consistent, parseable JSON responses - ✅ Proper `success`/`error` properties in all responses - ✅ Reliable client-server communication - ✅ Passing e2e tests +- ✅ No `"[object Object]"` serialization issues ### Why Both `body` and `requestBody` Properties? **Important**: You need BOTH properties for complete functionality: -- **`body`**: Enables Fastify's automatic request validation using the Zod schema +- **`body`**: Enables Fastify's automatic request validation using the JSON schema - **`requestBody`**: Ensures proper OpenAPI specification generation with Content-Type documentation Without `body`, validation won't work. Without `requestBody`, your API specification won't properly document the `application/json` Content-Type requirement. @@ -586,14 +775,13 @@ Without `body`, validation won't work. Without `requestBody`, your API specifica ```typescript // BAD: Manual validation (redundant) -const parsedBody = myRequestBodySchema.safeParse(request.body); -if (!parsedBody.success) { - return reply.status(400).send({ error: 'Invalid request body' }); +if (!request.body.name || !request.body.count) { + return reply.status(400).send({ error: 'Required fields missing' }); } // BAD: Manual field checks (redundant) -if (!request.body.name || !request.body.count) { - return reply.status(400).send({ error: 'Required fields missing' }); +if (request.body.name.length < 3) { + return reply.status(400).send({ error: 'Name too short' }); } // BAD: Manual enum validation (redundant) @@ -606,64 +794,64 @@ if (request.body.type !== 'mysql' && request.body.type !== 'sqlite') { ```typescript // GOOD: Trust the validation - if handler runs, data is valid -const { name, count, type } = request.body; // Already validated by Fastify +const { name, count, type } = request.body as RequestBody; // Already validated by Fastify ``` ### Validation Flow The validation chain works as follows: -#### Zod Schema → JSON Schema → Fastify Validation → Handler +#### JSON Schema → Fastify Validation → Handler -1. **Zod Schema**: Define validation rules using Zod -2. **JSON Schema**: Convert to OpenAPI format using `createSchema()` -3. **Fastify Validation**: Fastify automatically validates incoming requests -4. **Handler**: Receives validated, typed data +1. **JSON Schema**: Define validation rules using JSON Schema +2. **Fastify Validation**: Fastify automatically validates incoming requests +3. **Handler**: Receives validated, typed data If validation fails, Fastify automatically returns a 400 error **before** your handler runs. ### Real-World Examples -See these files for complete examples of proper Zod validation: +See these files for complete examples of proper validation: +- `src/routes/admin/email/test.ts` - **REFERENCE IMPLEMENTATION** showing complete reusable schema constants approach - `src/routes/db/setup.ts` - Database setup with enum validation - `src/routes/db/status.ts` - Simple GET endpoint with response schemas - `src/routes/auth/loginEmail.ts` - Login with required string fields - `src/routes/auth/registerEmail.ts` - Registration with complex validation rules -**Note**: Older examples in this document (like the "Logout Route Documentation" below) might still show manually crafted JSON schemas. The recommended approach is now to use Zod with automatic Fastify validation as shown above. - ## Example: Logout Route Documentation The logout route (`/api/auth/logout`) demonstrates proper documentation: ```typescript +const LOGOUT_RESPONSE_SCHEMA = { + type: 'object', + properties: { + success: { + type: 'boolean', + description: 'Indicates if the logout operation was successful' + }, + message: { + type: 'string', + description: 'Human-readable message about the logout result' + } + }, + required: ['success', 'message'], + examples: [ + { + success: true, + message: 'Logged out successfully.' + } + ] +} as const; + const logoutSchema = { tags: ['Authentication'], summary: 'User logout', description: 'Invalidates the current user session and clears authentication cookies', security: [{ cookieAuth: [] }], response: { - 200: { - type: 'object', - properties: { - success: { - type: 'boolean', - description: 'Indicates if the logout operation was successful' - }, - message: { - type: 'string', - description: 'Human-readable message about the logout result' - } - }, - required: ['success', 'message'], - examples: [ - { - success: true, - message: 'Logged out successfully.' - } - ] - } + 200: LOGOUT_RESPONSE_SCHEMA } }; ``` @@ -672,7 +860,7 @@ const logoutSchema = { ### Fastify Server Configuration -The Fastify server is configured with custom AJV options to ensure compatibility with `zod-openapi` schema generation. This configuration is in `src/server.ts`: +The Fastify server is configured with custom AJV options to ensure compatibility with JSON Schema validation. This configuration is in `src/server.ts`: ```typescript const server = fastify({ @@ -690,11 +878,11 @@ const server = fastify({ **Why these AJV options are required:** -- **`strict: false`**: AJV v8+ runs in strict mode by default, which rejects schemas containing unknown keywords. The `zod-openapi` library generates schemas that may include keywords AJV doesn't recognize in strict mode. -- **`strictTypes: false`**: Prevents strict type validation errors that can occur with complex Zod schemas. +- **`strict: false`**: AJV v8+ runs in strict mode by default, which rejects schemas containing unknown keywords. This setting allows more flexible schema definitions. +- **`strictTypes: false`**: Prevents strict type validation errors that can occur with complex schemas. - **`strictTuples: false`**: Allows more flexible tuple handling for array schemas. -**Important**: These settings don't affect validation behavior - they only allow the schema compilation to succeed. All validation rules defined in your Zod schemas still work exactly as expected. +**Important**: These settings don't affect validation behavior - they only allow the schema compilation to succeed. All validation rules defined in your JSON schemas still work exactly as expected. ### Swagger Configuration @@ -746,7 +934,7 @@ Routes without schema definitions will appear in the specification but with mini To extend API documentation: -1. Add schema definitions to more routes +1. Add schema definitions to more routes using reusable constants 2. Define reusable components in the OpenAPI configuration 3. Add request body schemas for POST/PUT endpoints 4. Include error response schemas (400, 401, 500, etc.) diff --git a/docs/development/frontend/architecture.mdx b/docs/development/frontend/architecture.mdx new file mode 100644 index 0000000..90a1260 --- /dev/null +++ b/docs/development/frontend/architecture.mdx @@ -0,0 +1,618 @@ +--- +title: Frontend Architecture +description: Comprehensive guide to DeployStack frontend application architecture, design patterns, and development principles +sidebar: Architecture +--- + +# Frontend Architecture + +This document defines the architectural principles, patterns, and conventions that govern the DeployStack frontend application. All developers must understand and follow these guidelines to maintain consistency and quality across the codebase. + +## Architectural Overview + +The DeployStack frontend follows a **feature-based modular architecture** with clear separation of concerns. The application is built on Vue 3's Composition API, emphasizing type safety, reusability, and maintainability. + +### Core Principles + +1. **Feature-First Organization**: Code is organized by feature domains rather than technical layers +2. **Type Safety First**: TypeScript is mandatory for all new code +3. **Composition Over Inheritance**: Use composables and the Composition API exclusively +4. **Direct API Communication**: No abstraction layers over fetch() calls +5. **Component-Driven Development**: Build from small, reusable components up to complex features + +## Directory Architecture + +### Views Layer (`/views`) + +Views represent **page-level components** that map directly to routes. They orchestrate the overall page functionality and data flow. + +#### Organization Rules + +1. **Route Mapping**: Each view corresponds to a specific route in the application +2. **Nested Structure**: Mirror the URL structure in the directory hierarchy +3. **Feature Grouping**: Group related views in subdirectories + +``` +views/ +├── admin/ # Admin-only views +│ ├── mcp-categories/ # Category management feature +│ │ └── index.vue # Main listing page +│ └── mcp-server-catalog/ # Catalog management feature +│ ├── index.vue # Listing +│ ├── add.vue # Creation +│ └── edit/[id].vue # Dynamic editing +├── teams/ # Team management feature +│ ├── index.vue # Teams listing +│ └── manage/[id].vue # Team management page +└── Dashboard.vue # Top-level dashboard +``` + +#### View Responsibilities + +- **Route handling**: Process route parameters and query strings +- **Data orchestration**: Coordinate multiple service calls +- **Layout selection**: Choose appropriate layout wrapper +- **Permission checks**: Verify user access rights +- **Error boundaries**: Handle page-level errors + +#### What Views Should NOT Do + +- Contain complex business logic (use services) +- Implement reusable UI patterns (use components) +- Directly manage global state (use stores) +- Include detailed form validation (use composables) + +### Components Layer (`/components`) + +Components are **reusable UI building blocks** that encapsulate specific functionality and presentation logic. + +#### Component Categories + +1. **UI Components** (`/ui`): Generic, design-system components - read the [UI Design System](/development/frontend/ui-design-system) + - Examples: Buttons, Modals, Inputs + - Stateless and focused on presentation + - Use shadcn-vue components where applicable + - Follow shadcn-vue patterns + - No business logic + - Highly reusable across features + - Follow shadcn-vue patterns + +2. **Feature Components** (`/components/[feature]`): Domain-specific components + - Contain feature-specific logic + - Reusable within their domain + - May compose UI components + +3. **Shared Components** (`/components`): Cross-feature components + - Used across multiple features + - Examples: `AppSidebar`, `DashboardLayout` + +#### Component Design Rules + +1. **Single Responsibility**: Each component has one clear purpose +2. **Props Down, Events Up**: Maintain unidirectional data flow +3. **Composition Pattern**: Break complex components into smaller parts +4. **Self-Contained**: Components should work in isolation + +### Services Layer (`/services`) + +Services handle **all external communication and business logic processing**. They act as the bridge between the frontend and backend APIs. + +#### Service Architecture Patterns + +1. **Static Class Pattern**: All service methods must be static +2. **Direct Fetch Usage**: Use native fetch() API exclusively +3. **Type-Safe Contracts**: Define interfaces for all API requests/responses +4. **Error Transformation**: Convert API errors to user-friendly messages + +#### Service Responsibilities + +- API endpoint communication +- Request/response transformation +- Error handling and normalization +- Cache management (when applicable) +- Business logic that spans multiple components + +### Composables Layer (`/composables`) + +Composables are **reusable logic units** that leverage Vue's Composition API to share stateful logic across components. + +#### Composable Design Patterns + +1. **Naming Convention**: Always prefix with `use` (e.g., `useAuth`, `useEventBus`) +2. **Single Purpose**: Each composable solves one specific problem +3. **Return Interface**: Clearly define what's returned (state, methods, computed) +4. **Lifecycle Awareness**: Handle setup/cleanup in lifecycle hooks + +#### Common Composable Patterns + +- **Data Fetching**: `useAsyncData`, `usePagination` +- **Form Handling**: `useForm`, `useValidation` +- **UI State**: `useModal`, `useToast` +- **Feature Logic**: `useTeamManagement`, `useCredentials` + +### Stores Layer (`/stores`) + +Stores manage **global application state** using Pinia, Vue's official state management solution. + +#### Store Guidelines + +1. **Feature-Based Stores**: One store per major feature domain +2. **Composition API Style**: Use setup stores, not options API +3. **Readonly State**: Export readonly refs to prevent external mutations +4. **Action Pattern**: All state changes through defined actions + +#### When to Use Stores + +- User session and authentication state +- Cross-component shared data +- Cache for expensive operations +- Application-wide settings + +#### When NOT to Use Stores + +- Component-specific state +- Temporary UI state +- Form data (use local state) + +## API Integration Architecture + +### Service Layer Pattern + +**IMPORTANT**: The frontend uses a service layer pattern with direct `fetch()` calls for API communication. This is the established pattern and must be followed for consistency. + +#### ✅ Required Pattern - Direct Fetch Calls + +All API services must use direct `fetch()` calls instead of API client libraries. This ensures consistency across the codebase and simplifies maintenance. + +```typescript +// services/mcpServerService.ts +export class McpServerService { + private static baseUrl = getEnv('VITE_API_URL') + + static async getAllServers(): Promise { + const response = await fetch(`${this.baseUrl}/api/mcp-servers`) + if (!response.ok) { + throw new Error('Failed to fetch MCP servers') + } + return response.json() + } + + static async deployServer(serverId: string, config: DeployConfig): Promise { + const response = await fetch(`${this.baseUrl}/api/mcp-servers/${serverId}/deploy`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(config), + }) + + if (!response.ok) { + throw new Error('Failed to deploy MCP server') + } + + return response.json() + } +} +``` + +#### ❌ Avoid - API Client Libraries + +Do not use API client libraries like Axios, or custom API client wrappers: + +```typescript +// DON'T DO THIS +import axios from 'axios' +import { apiClient } from '@/utils/apiClient' + +// Avoid these patterns +const response = await axios.get('/api/servers') +const data = await apiClient.get('/api/servers') +``` + +#### Service Layer Guidelines + +1. **Use Static Classes**: All service methods should be static +2. **Direct Fetch**: Always use native `fetch()` API +3. **Error Handling**: Throw meaningful errors for failed requests +4. **Type Safety**: Define proper TypeScript interfaces for requests/responses +5. **Consistent Naming**: Use descriptive method names (e.g., `getAllServers`, `createCategory`) +6. **Base URL**: Always use environment variables for API endpoints + +### Using Services in Components + +```vue + +``` + +## Data Flow Architecture + +### Unidirectional Data Flow + +```bash +User Interaction → View → Service → API + ↓ + Component ← Store ← Response +``` + +1. **User triggers action** in a View or Component +2. **View/Component calls Service** method +3. **Service communicates with API** using fetch() +4. **Response updates Store** (if global state) +5. **Components react** to store changes + +### Event-Driven Updates + +The application uses an **event bus** for cross-component communication without direct coupling. This enables real-time updates across unrelated components, cache invalidation signals, global notifications, and feature-to-feature communication. + +For complete details on the event bus system, including usage patterns, naming conventions, and implementation examples, see the [Event Bus Documentation](/development/frontend/event-bus). + +### Persistent State Management + +The application includes a **storage system** built into the event bus for managing persistent state across route changes and browser sessions. This system provides type-safe localStorage access with automatic event emission for reactive updates. + +For complete details on the storage system, including usage patterns, naming conventions, and best practices, see the [Frontend Storage System](/development/frontend/storage). + +## Component Implementation Standards + +### Vue Component Structure + +**Always prefer Vue Single File Components (SFC) with ` + + +``` + +#### ❌ Avoid - TypeScript files with render functions: + +```typescript +// Don't create files like this for UI components +import { h } from 'vue' +import type { ColumnDef } from '@tanstack/vue-table' + +export function createColumns(): ColumnDef[] { + return [ + { + id: 'actions', + cell: ({ row }) => { + return h('div', { class: 'flex justify-end' }, [ + h(Button, { + onClick: () => handleAction(row.original.id) + }, () => 'Action') + ]) + } + } + ] +} +``` + +#### Why Vue SFC is Preferred + +1. **Better Developer Experience**: Clear separation of logic, template, and styles +2. **Improved Readability**: Template syntax is more intuitive than render functions +3. **Better Tooling Support**: Vue DevTools, syntax highlighting, and IntelliSense work better +4. **Easier Maintenance**: Future developers can understand and modify components more easily +5. **Vue 3 Best Practices**: Aligns with official Vue 3 recommendations + +### Table Components + +For table implementations, use the shadcn-vue Table components as documented in the [Table Design System](/development/frontend/ui-design-system-table). Never use raw HTML table elements. + +## Component Communication Patterns + +### Parent-Child Communication + +1. **Props for Data Down**: Pass data from parent to child +2. **Events for Actions Up**: Emit events from child to parent +3. **v-model for Two-Way**: Use for form inputs and controlled components + +### Sibling Communication + +1. **Through Parent**: Lift state up to common parent +2. **Event Bus**: For loosely coupled components +3. **Shared Store**: For persistent shared state + +### Cross-Feature Communication + +1. **Event Bus**: Primary method for feature-to-feature updates +2. **Shared Services**: Common API operations +3. **Global Store**: Application-wide state + +## Form Architecture + +### Form Handling Strategy + +1. **Local State First**: Keep form data in component state +2. **Validation Composables**: Reuse validation logic +3. **Service Layer Submission**: Process through services +4. **Error Display Pattern**: Consistent error messaging + +### Form Patterns + +- Use VeeValidate with Zod schemas for complex forms +- Implement field-level validation feedback +- Show loading states during submission +- Handle API validation errors gracefully + +## Routing Architecture + +### Route Organization + +1. **Feature Modules**: Group related routes by feature +2. **Lazy Loading**: Use dynamic imports for route components +3. **Route Guards**: Implement authentication and authorization checks +4. **Breadcrumb Support**: Maintain hierarchical navigation + +### Dynamic Routes + +- Use `[id]` notation for dynamic segments +- Handle route parameter validation in views +- Implement proper 404 handling for invalid IDs + +## Error Handling Architecture + +### Error Boundaries + +1. **View Level**: Catch and display page-level errors +2. **Component Level**: Handle component-specific errors +3. **Global Level**: Catch unhandled errors + +### Error Patterns + +- Display user-friendly error messages +- Log technical details for debugging +- Provide recovery actions when possible +- Maintain application stability on errors + +## Performance Architecture + +### Code Splitting Strategy + +1. **Route-Based Splitting**: Each route loads its own bundle +2. **Component Lazy Loading**: Heavy components load on demand +3. **Vendor Chunking**: Separate third-party libraries + +### Optimization Patterns + +- Use `shallowRef` for large objects +- Implement virtual scrolling for long lists +- Debounce expensive operations +- Memoize computed values appropriately + +## Security Architecture + +### Frontend Security Principles + +1. **Never Trust Client**: All validation must happen on backend +2. **Secure Storage**: Never store sensitive data (passwords, API keys, tokens) in localStorage. See [Frontend Storage System](/development/frontend/storage) for proper storage patterns +3. **XSS Prevention**: Sanitize user input, use Vue's built-in protections +4. **CSRF Protection**: Include tokens in API requests + +### Authentication Flow + +- Token-based authentication (JWT) +- Automatic token refresh +- Secure token storage (httpOnly cookies preferred) +- Route protection via navigation guards + +## Testing Architecture + +### Testing Strategy + +1. **Unit Tests**: For services, composables, and utilities +2. **Component Tests**: For isolated component behavior +3. **Integration Tests**: For feature workflows +4. **E2E Tests**: For critical user paths + +### Test Organization + +- Mirror source structure in test directories +- Co-locate test files with source files +- Use descriptive test names +- Follow AAA pattern (Arrange, Act, Assert) + +## Plugin Architecture + +### Plugin System Design + +The application supports runtime plugin loading for extensibility. + +#### Plugin Structure + +1. **Entry Point**: Each plugin exports a default configuration +2. **Extension Points**: Plugins hook into defined extension points +3. **Isolation**: Plugins run in isolated contexts +4. **Version Management**: Plugins declare compatible versions + +#### Plugin Guidelines + +- Plugins cannot modify core functionality +- Use provided APIs and extension points +- Handle errors gracefully +- Document dependencies clearly + +## Development Workflow + +### Code Organization Rules + +1. **Feature Cohesion**: Keep related code together +2. **Explicit Imports**: No magic globals or auto-imports +3. **Type Definitions**: Colocate types with their usage +4. **Consistent Naming**: Follow established patterns + +### File Naming Conventions + +- **Components**: PascalCase (e.g., `UserProfile.vue`) +- **Composables**: camelCase with 'use' prefix (e.g., `useAuth.ts`) +- **Services**: camelCase with 'Service' suffix (e.g., `userService.ts`) +- **Types**: PascalCase for interfaces/types (e.g., `UserCredentials`) +- **Views**: Match route names (e.g., `index.vue`, `[id].vue`) + +### Import Order + +1. External dependencies +2. Vue and framework imports +3. Internal aliases (@/ imports) +4. Relative imports +5. Type imports + +## Anti-Patterns to Avoid + +### Component Anti-Patterns + +- ❌ Using Options API in new components +- ❌ Mixing paradigms (Options + Composition) +- ❌ Direct DOM manipulation +- ❌ Inline styles for layout +- ❌ Business logic in templates + +### State Management Anti-Patterns + +- ❌ Mutating props directly +- ❌ Excessive global state +- ❌ Circular store dependencies +- ❌ Store logic in components + +### Service Anti-Patterns + +- ❌ Using Axios or other HTTP libraries +- ❌ Instance-based service classes +- ❌ Mixing UI concerns in services +- ❌ Inconsistent error handling + +### General Anti-Patterns + +- ❌ Premature optimization +- ❌ Deep component nesting (>3 levels) +- ❌ Tight coupling between features +- ❌ Ignoring TypeScript errors +- ❌ Copy-paste programming + +## Architecture Decision Records + +### Why Static Services? + +Static service methods ensure: +- No instance management complexity +- Predictable behavior +- Easy testing and mocking +- Clear API boundaries + +### Why Direct Fetch? + +Using native fetch() provides: +- No external dependencies +- Consistent API across services +- Full control over request/response +- Smaller bundle size + +### Why Feature-Based Structure? + +Feature organization offers: +- Better code locality +- Easier feature removal/addition +- Clear ownership boundaries +- Reduced merge conflicts + +## Migration Guidelines + +When refactoring existing code: + +1. **Incremental Migration**: Update feature by feature +2. **Test Coverage First**: Add tests before refactoring +3. **Preserve Functionality**: No behavior changes during refactor +4. **Document Changes**: Update relevant documentation +5. **Review Thoroughly**: Architecture changes need careful review + +## Future Considerations + +As the application grows, consider: + +- Micro-frontend architecture for team autonomy +- Module federation for dynamic feature loading +- GraphQL adoption for efficient data fetching +- Server-side rendering for performance +- Progressive Web App capabilities + +## Conclusion + +This architecture provides a scalable, maintainable foundation for the DeployStack frontend. Following these patterns ensures consistency, reduces bugs, and improves developer productivity. When in doubt, prioritize clarity and simplicity over clever solutions. + +Remember: **Architecture is a team effort**. Propose improvements, discuss trade-offs, and evolve these patterns as the application grows. \ No newline at end of file diff --git a/docs/development/frontend/event-bus.mdx b/docs/development/frontend/event-bus.mdx index 2ac1dbe..c21fab4 100644 --- a/docs/development/frontend/event-bus.mdx +++ b/docs/development/frontend/event-bus.mdx @@ -70,7 +70,7 @@ eventBus.on('storage-changed', (data) => { }) ``` -> **📖 For detailed storage usage, see [Frontend Storage System](./storage)** +> **📖 For detailed storage usage, see [Frontend Storage System](/development/frontend/storage)** ## Usage @@ -321,28 +321,57 @@ onUnmounted(() => { ``` -## Best Practices +## Event Naming Convention + +The event bus follows a consistent naming pattern to ensure clarity and maintainability across the application. -### 1. Event Naming Convention +### Standard Pattern -Use descriptive, action-based names with consistent patterns: +``` +{feature}-{action} +``` + +This pattern makes events self-documenting and easy to understand at a glance. + +### Examples ```typescript -// Good +// Team-related events 'teams-updated' -'user-profile-changed' +'team-created' +'team-deleted' +'team-selected' + +// Credential events +'credentials-updated' +'credential-created' +'credential-deleted' + +// MCP server events 'mcp-server-deployed' -'notification-show' +'mcp-server-installed' +'mcp-installation-removed' -// Avoid -'update' -'change' -'event1' +// System events +'notification-show' +'storage-changed' +'cache-cleared' ``` -### 2. Type Safety +### Naming Guidelines + +- **Use kebab-case**: All event names should use lowercase with hyphens +- **Feature first**: Start with the feature or domain name +- **Action second**: End with the specific action or state change +- **Be specific**: Avoid generic names like 'update' or 'change' without context +- **Past tense for completed actions**: Use 'created', 'updated', 'deleted' for completed operations +- **Present tense for commands**: Use 'show', 'hide', 'clear' for immediate actions + +## Best Practices + +### 1. Event Type Definition -Always define event types in the `EventBusEvents` interface: +Always define event types in the `EventBusEvents` interface for type safety: ```typescript export type EventBusEvents = { @@ -357,7 +386,7 @@ export type EventBusEvents = { } ``` -### 3. Memory Management +### 2. Memory Management Always clean up event listeners to prevent memory leaks: @@ -385,7 +414,7 @@ onUnmounted(() => { ``` -### 4. Error Handling +### 3. Error Handling Wrap event handlers in try-catch blocks: @@ -405,7 +434,7 @@ const handleDataUpdate = (data: any) => { } ``` -### 5. Debugging Events +### 4. Debugging Events Add logging for development: diff --git a/docs/development/frontend/index.mdx b/docs/development/frontend/index.mdx index 2a33fda..8f4852f 100644 --- a/docs/development/frontend/index.mdx +++ b/docs/development/frontend/index.mdx @@ -52,23 +52,7 @@ The development server will start at `http://localhost:5173` with hot module rep ## Project Structure -```bash -services/frontend/ -├── src/ -│ ├── components/ # Reusable Vue components -│ ├── views/ # Page-level components -│ ├── router/ # Vue Router configuration -│ ├── stores/ # Pinia stores (state management) -│ ├── services/ # API services and utilities -│ ├── plugins/ # Frontend plugin system -│ ├── i18n/ # Internationalization files -│ ├── utils/ # Utility functions -│ ├── types/ # TypeScript type definitions -│ └── assets/ # Static assets -├── public/ # Public static files -├── dist/ # Built application (generated) -└── ... -``` +The frontend follows a feature-based modular architecture with clear separation of concerns. For a comprehensive understanding of the application architecture, directory organization, and design patterns, see the [Frontend Architecture Guide](/development/frontend/architecture). ## Development Guidelines @@ -82,161 +66,9 @@ services/frontend/ ### Component Development -#### Vue Component Structure - -**Always prefer Vue Single File Components (SFC) with ` - - -``` - -❌ **Avoid - TypeScript files with render functions:** +For comprehensive component development guidelines, including Vue SFC best practices, component structure patterns, and implementation standards, see the [Frontend Architecture - Component Implementation Standards](/development/frontend/architecture#component-implementation-standards) section. -```typescript -// Don't create files like this for UI components -import { h } from 'vue' -import type { ColumnDef } from '@tanstack/vue-table' - -export function createColumns(): ColumnDef[] { - return [ - { - id: 'actions', - cell: ({ row }) => { - return h('div', { class: 'flex justify-end' }, [ - h(Button, { - onClick: () => handleAction(row.original.id) - }, () => 'Action') - ]) - } - } - ] -} -``` - -#### Why Vue SFC is Preferred - -1. **Better Developer Experience**: Clear separation of logic, template, and styles -2. **Improved Readability**: Template syntax is more intuitive than render functions -3. **Better Tooling Support**: Vue DevTools, syntax highlighting, and IntelliSense work better -4. **Easier Maintenance**: Future developers can understand and modify components more easily -5. **Vue 3 Best Practices**: Aligns with official Vue 3 recommendations - -#### Table Components Example - -When creating table components, prefer this structure: - -```vue - - - -``` +For table-specific implementations, refer to the [Table Design System](/development/frontend/ui-design-system-table) guide. ## UI Components and Styling @@ -275,104 +107,13 @@ const allEnvVars = getAllEnv() ## API Integration -### Service Layer Pattern - -**IMPORTANT**: The frontend uses a service layer pattern with direct `fetch()` calls for API communication. This is the established pattern and must be followed for consistency. - -#### ✅ Required Pattern - Direct Fetch Calls - -All API services must use direct `fetch()` calls instead of API client libraries. This ensures consistency across the codebase and simplifies maintenance. - -```typescript -// services/mcpServerService.ts -export class McpServerService { - private static baseUrl = getEnv('VITE_API_URL') - - static async getAllServers(): Promise { - const response = await fetch(`${this.baseUrl}/api/mcp-servers`) - if (!response.ok) { - throw new Error('Failed to fetch MCP servers') - } - return response.json() - } - - static async deployServer(serverId: string, config: DeployConfig): Promise { - const response = await fetch(`${this.baseUrl}/api/mcp-servers/${serverId}/deploy`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(config), - }) - - if (!response.ok) { - throw new Error('Failed to deploy MCP server') - } - - return response.json() - } -} -``` - -#### ❌ Avoid - API Client Libraries - -Do not use API client libraries like Axios, or custom API client wrappers: - -```typescript -// DON'T DO THIS -import axios from 'axios' -import { apiClient } from '@/utils/apiClient' - -// Avoid these patterns -const response = await axios.get('/api/servers') -const data = await apiClient.get('/api/servers') -``` - -#### Service Layer Guidelines - -1. **Use Static Classes**: All service methods should be static -2. **Direct Fetch**: Always use native `fetch()` API -3. **Error Handling**: Throw meaningful errors for failed requests -4. **Type Safety**: Define proper TypeScript interfaces for requests/responses -5. **Consistent Naming**: Use descriptive method names (e.g., `getAllServers`, `createCategory`) -6. **Base URL**: Always use environment variables for API endpoints - -### Using Services in Components - -```vue - -``` +The frontend uses a service layer pattern with direct `fetch()` calls for API communication. For complete API integration details including service patterns, implementation examples, and guidelines, see the [Frontend Architecture - API Integration Architecture](/development/frontend/architecture#api-integration-architecture) section. ## Next Steps Continue reading the detailed guides: +- [Frontend Architecture](/development/frontend/architecture) - Application architecture, patterns, and development principles - [UI Design System](/development/frontend/ui-design-system) - Component patterns, styling guidelines, and design standards - [Environment Variables](/development/frontend/environment-variables) - Complete environment configuration guide - [Global Event Bus](/development/frontend/event-bus) - Cross-component communication system diff --git a/docs/development/frontend/internationalization.mdx b/docs/development/frontend/internationalization.mdx index 2ba4c55..3c69200 100644 --- a/docs/development/frontend/internationalization.mdx +++ b/docs/development/frontend/internationalization.mdx @@ -1,882 +1,164 @@ --- title: Internationalization (i18n) -description: Guide to implementing multi-language support in DeployStack frontend using Vue I18n with modular file structure. +description: Quick reference for working with i18n in DeployStack frontend --- # Internationalization (i18n) -DeployStack supports multiple languages through Vue I18n with a modular file structure that organizes translations by feature. This approach makes it easy to maintain translations and add new languages. - -## Architecture Overview - -The i18n system is designed with modularity and maintainability in mind: - -- **Feature-based organization**: Translations are grouped by application features -- **Modular structure**: Each feature has its own translation file -- **Scalable approach**: Easy to add new languages and features -- **Type safety**: TypeScript support for translation keys - -## Directory Structure - -```bash -src/i18n/ -├── index.ts # Main i18n initialization -└── locales/ - ├── en/ # English translations - │ ├── index.ts # Exports all English translations - │ ├── common.ts # Common translations (buttons, labels, etc.) - │ ├── login.ts # Login page specific translations - │ ├── register.ts # Register page specific translations - │ ├── dashboard.ts # Dashboard specific translations - │ ├── mcp-servers.ts # MCP server management translations - │ └── deployment.ts # Deployment related translations - └── de/ # German translations (example) - ├── index.ts - ├── common.ts - └── ... -``` - -## Setting Up i18n - -### Main Configuration - -The main i18n configuration in `src/i18n/index.ts`: - -```typescript -import { createI18n } from 'vue-i18n' -import en from './locales/en' -import de from './locales/de' // Example additional language - -const i18n = createI18n({ - legacy: false, // Use Composition API - locale: 'en', // Default language - fallbackLocale: 'en', // Fallback language - messages: { - en, - de - } -}) - -export default i18n -``` - -### Locale Index File - -Each locale has an index file that exports all translations: - -```typescript -// src/i18n/locales/en/index.ts -import common from './common' -import login from './login' -import register from './register' -import dashboard from './dashboard' -import mcpServers from './mcp-servers' -import deployment from './deployment' - -export default { - common, - login, - register, - dashboard, - mcpServers, - deployment -} -``` - -## Creating Translation Files - -### Common Translations - -File: `src/i18n/locales/en/common.ts` - -```typescript -export default { - // Navigation - navigation: { - dashboard: 'Dashboard', - mcpServers: 'MCP Servers', - deployments: 'Deployments', - settings: 'Settings', - logout: 'Logout' - }, - - // Common buttons and actions - buttons: { - save: 'Save', - cancel: 'Cancel', - delete: 'Delete', - edit: 'Edit', - create: 'Create', - deploy: 'Deploy', - stop: 'Stop', - restart: 'Restart', - view: 'View', - download: 'Download' - }, - - // Status indicators - status: { - running: 'Running', - stopped: 'Stopped', - error: 'Error', - pending: 'Pending', - deploying: 'Deploying', - healthy: 'Healthy', - unhealthy: 'Unhealthy' - }, - - // Common labels - labels: { - name: 'Name', - description: 'Description', - status: 'Status', - created: 'Created', - updated: 'Updated', - version: 'Version', - author: 'Author', - email: 'Email', - password: 'Password' - }, - - // Validation messages - validation: { - required: 'This field is required', - email: 'Please enter a valid email address', - minLength: 'Must be at least {min} characters', - maxLength: 'Must be no more than {max} characters', - passwordMismatch: 'Passwords do not match' - }, - - // Common messages - messages: { - loading: 'Loading...', - saving: 'Saving...', - saved: 'Saved successfully', - error: 'An error occurred', - noData: 'No data available', - confirmDelete: 'Are you sure you want to delete this item?' - } -} -``` - -### Feature-Specific Translations +DeployStack uses Vue I18n with a modular file structure. Translations are organized by feature for easy maintenance. -File: `src/i18n/locales/en/mcp-servers.ts` +## Architecture -```typescript -export default { - title: 'MCP Servers', - subtitle: 'Manage your Model Context Protocol servers', - - catalog: { - title: 'MCP Server Catalog', - description: 'Browse and deploy MCP servers from our community catalog', - search: 'Search servers...', - categories: { - all: 'All Categories', - databases: 'Databases', - apis: 'APIs', - tools: 'Development Tools', - productivity: 'Productivity', - integrations: 'Integrations' - } - }, - - deployment: { - title: 'Deploy MCP Server', - selectProvider: 'Select Cloud Provider', - configure: 'Configure Deployment', - credentials: { - title: 'Credentials', - description: 'Configure API keys and authentication', - addCredential: 'Add Credential', - apiKey: 'API Key', - secretKey: 'Secret Key', - token: 'Access Token' - }, - environment: { - title: 'Environment Variables', - description: 'Configure environment variables for your MCP server', - addVariable: 'Add Variable', - key: 'Variable Name', - value: 'Variable Value' - } - }, - - management: { - myServers: 'My Deployed Servers', - serverDetails: 'Server Details', - logs: 'View Logs', - metrics: 'Performance Metrics', - scale: 'Scale Server', - instances: 'Instances', - uptime: 'Uptime', - lastDeployed: 'Last Deployed' - }, - - forms: { - serverName: { - label: 'Server Name', - placeholder: 'Enter a name for your server deployment', - description: 'This will be used to identify your deployment' - }, - region: { - label: 'Deployment Region', - placeholder: 'Select region', - description: 'Choose the region closest to your users' - } - }, - - actions: { - deployNow: 'Deploy Now', - viewInCatalog: 'View in Catalog', - manageServer: 'Manage Server', - viewLogs: 'View Logs', - stopServer: 'Stop Server', - restartServer: 'Restart Server', - deleteDeployment: 'Delete Deployment' - }, - - notifications: { - deploymentStarted: 'Deployment started successfully', - deploymentCompleted: 'Server deployed successfully', - deploymentFailed: 'Deployment failed: {error}', - serverStopped: 'Server stopped successfully', - serverRestarted: 'Server restarted successfully', - serverDeleted: 'Server deployment deleted' - } -} -``` +- **Location**: `src/i18n/` +- **Current language**: English only (`en`) +- **Structure**: Feature-based translation files +- **Framework**: Vue I18n with Composition API (non-legacy mode) -## Using Translations in Components +## Basic Usage -### Basic Usage +### Using in Components ```vue - - -``` - -### Advanced Usage with Computed Properties -```vue - ``` -### Form Validation with i18n - -```vue - - - -``` - -## Adding New Languages - -### 1. Create Language Directory - -```bash -mkdir -p src/i18n/locales/de -``` - -### 2. Create Translation Files - -Start with the common translations: +Find the appropriate file in `src/i18n/locales/en/` and add your translation: ```typescript -// src/i18n/locales/de/common.ts +// Example: Adding to globalSettings.ts export default { - navigation: { - dashboard: 'Dashboard', - mcpServers: 'MCP Server', - deployments: 'Bereitstellungen', - settings: 'Einstellungen', - logout: 'Abmelden' - }, - - buttons: { - save: 'Speichern', - cancel: 'Abbrechen', - delete: 'Löschen', - edit: 'Bearbeiten', - create: 'Erstellen', - deploy: 'Bereitstellen', - stop: 'Stoppen', - restart: 'Neu starten', - view: 'Anzeigen', - download: 'Herunterladen' - }, - - // ... continue with other translations -} -``` - -### 3. Create Locale Index - -```typescript -// src/i18n/locales/de/index.ts -import common from './common' -import login from './login' -import register from './register' -// ... import other feature translations - -export default { - common, - login, - register, - // ... export other translations -} -``` - -### 4. Update Main i18n Configuration - -```typescript -// src/i18n/index.ts -import en from './locales/en' -import de from './locales/de' - -const i18n = createI18n({ - legacy: false, - locale: 'en', - fallbackLocale: 'en', - messages: { - en, - de // Add the new language + errors: { + configNotSet: 'Configuration not set', + fetchFailed: 'Failed to fetch settings', + // Add your new translation here + myNewError: 'Something went wrong with {item}' } -}) +} ``` -## Language Switching - -### Language Selector Component +### 2. Use in Component ```vue - - ``` -## Best Practices - -### 1. Translation Key Naming - -Use descriptive, hierarchical naming: - -```typescript -// Good -'mcpServers.deployment.credentials.title' -'common.validation.passwordTooShort' -'dashboard.widgets.serverStatus.healthy' - -// Avoid -'title' -'error1' -'msg' -``` +## Creating a New Feature Translation File -### 2. Parameterized Messages +When adding a new feature, create a dedicated translation file: -Use parameters for dynamic content: +### 1. Create the Translation File ```typescript -// Translation file +// src/i18n/locales/en/myFeature.ts export default { + title: 'My Feature', + description: 'Feature description', + actions: { + submit: 'Submit', + cancel: 'Cancel' + }, messages: { - welcomeUser: 'Welcome back, {userName}!', - itemsCount: 'You have {count} {count, plural, one {item} other {items}}', - deploymentTime: 'Deployed {timeAgo} ago' + success: 'Operation successful', + error: 'An error occurred: {message}' } } - -// Usage -t('messages.welcomeUser', { userName: 'John' }) -t('messages.itemsCount', { count: 5 }) ``` -### 3. Context-Aware Translations - -Group related translations together: +### 2. Register in Index ```typescript -export default { - serverStatus: { - running: { - label: 'Running', - description: 'Server is operating normally', - action: 'Stop Server' - }, - stopped: { - label: 'Stopped', - description: 'Server is not running', - action: 'Start Server' - } - } -} -``` - -### 4. Handle Missing Translations - -Always provide fallback values: - -```vue - -``` - -## TypeScript Integration - -### Translation Key Types - -Create type definitions for better IDE support: - -```typescript -// types/i18n.ts -export interface I18nMessages { - common: { - buttons: { - save: string - cancel: string - // ... other button translations - } - // ... other common translations - } - mcpServers: { - title: string - deployment: { - title: string - // ... other deployment translations - } - // ... other MCP server translations - } -} - -// Extend the vue-i18n module -declare module 'vue-i18n' { - export interface DefineLocaleMessage extends I18nMessages {} -} -``` - -### Typed Translation Function - -```typescript -import { useI18n } from 'vue-i18n' -import type { I18nMessages } from '@/types/i18n' +// src/i18n/locales/en/index.ts +import myFeatureMessages from './myFeature' -// Create a typed version of the translation function -export function useTypedI18n() { - const { t, ...rest } = useI18n() - return { t, ...rest } +export default { + // ... existing imports + myFeature: myFeatureMessages } ``` -## Testing Translations - -### Testing Translation Keys - -```typescript -// tests/i18n.test.ts -import { describe, it, expect } from 'vitest' -import en from '@/i18n/locales/en' - -describe('i18n translations', () => { - it('should have all required common translations', () => { - expect(en.common.buttons.save).toBeDefined() - expect(en.common.buttons.cancel).toBeDefined() - expect(en.common.validation.required).toBeDefined() - }) - - it('should have MCP server translations', () => { - expect(en.mcpServers.title).toBeDefined() - expect(en.mcpServers.deployment.title).toBeDefined() - }) -}) -``` +## Key Naming Convention -### Component Translation Testing +### Structure -```typescript -// tests/components/LanguageSelector.test.ts -import { mount } from '@vue/test-utils' -import { createI18n } from 'vue-i18n' -import LanguageSelector from '@/components/LanguageSelector.vue' - -const i18n = createI18n({ - legacy: false, - locale: 'en', - messages: { - en: { test: 'Test' }, - de: { test: 'Test' } - } -}) - -describe('LanguageSelector', () => { - it('should change language when selected', async () => { - const wrapper = mount(LanguageSelector, { - global: { - plugins: [i18n] - } - }) - - const select = wrapper.find('select') - await select.setValue('de') - - expect(i18n.global.locale.value).toBe('de') - }) -}) ``` - -## Performance Considerations - -### Lazy Loading Translations - -For large applications, consider lazy loading translation files: - -```typescript -// src/i18n/index.ts -import { createI18n } from 'vue-i18n' - -const i18n = createI18n({ - legacy: false, - locale: 'en', - fallbackLocale: 'en', - messages: {} -}) - -// Lazy load function -async function loadLocaleMessages(locale: string) { - if (i18n.global.availableLocales.includes(locale)) { - return - } - - try { - const messages = await import(`./locales/${locale}/index.ts`) - i18n.global.setLocaleMessage(locale, messages.default) - } catch (error) { - console.error(`Failed to load locale ${locale}:`, error) - } -} - -// Usage in components -export async function setLanguage(locale: string) { - await loadLocaleMessages(locale) - i18n.global.locale.value = locale -} +{feature}.{section}.{key} ``` -### Translation Caching - -Implement caching for frequently used translations: +Examples: +- `globalSettings.errors.configNotSet` +- `mcpCatalog.filters.byCategory` +- `common.buttons.save` +- `validation.required` -```typescript -// utils/translationCache.ts -const cache = new Map() +### Rules -export function getCachedTranslation(key: string, params?: any): string | null { - const cacheKey = `${key}-${JSON.stringify(params || {})}` - return cache.get(cacheKey) || null -} - -export function setCachedTranslation(key: string, params: any, value: string): void { - const cacheKey = `${key}-${JSON.stringify(params || {})}` - cache.set(cacheKey, value) -} -``` +1. **Use camelCase** for keys +2. **Group by feature** (globalSettings, mcpCatalog, teams) +3. **Use descriptive names** (avoid generic keys like 'msg1', 'error1') +4. **Common translations** go in `common.ts` +5. **Validation messages** can be shared or feature-specific -## Common Patterns +## Parameters and Pluralization -### Form Validation Messages +### Parameters ```typescript -// i18n/locales/en/validation.ts -export default { - serverName: { - required: 'Server name is required', - minLength: 'Server name must be at least 3 characters', - maxLength: 'Server name cannot exceed 50 characters', - invalidChars: 'Server name can only contain letters, numbers, and hyphens' - }, - deployment: { - region: { - required: 'Please select a deployment region', - invalid: 'Selected region is not available' - }, - credentials: { - apiKey: { - required: 'API key is required', - invalid: 'Invalid API key format' - } - } - } +// Translation +messages: { + deploymentFailed: 'Deployment failed: {error}', + userGreeting: 'Welcome back, {userName}!' } -``` - -### Status and Notification Messages -```typescript -// i18n/locales/en/notifications.ts -export default { - success: { - serverDeployed: 'MCP server deployed successfully', - configurationSaved: 'Configuration saved', - credentialsUpdated: 'Credentials updated securely' - }, - error: { - deploymentFailed: 'Deployment failed: {reason}', - networkError: 'Network connection error. Please try again.', - unauthorized: 'You are not authorized to perform this action', - serverNotFound: 'Server not found or has been deleted' - }, - warning: { - unsavedChanges: 'You have unsaved changes. Are you sure you want to leave?', - serverRestarting: 'Server is restarting. This may take a few minutes.', - quotaExceeded: 'You have reached your deployment quota' - } -} +// Usage +t('messages.deploymentFailed', { error: 'Invalid credentials' }) +t('messages.userGreeting', { userName: user.name }) ``` -### Navigation and Menu Items +### Pluralization ```typescript -// i18n/locales/en/navigation.ts -export default { - main: { - dashboard: 'Dashboard', - catalog: 'MCP Catalog', - deployments: 'My Deployments', - teams: 'Teams', - settings: 'Settings' - }, - breadcrumbs: { - home: 'Home', - mcpServers: 'MCP Servers', - deployment: 'Deployment', - configuration: 'Configuration' - }, - actions: { - deploy: 'Deploy Server', - configure: 'Configure', - manage: 'Manage', - monitor: 'Monitor', - scale: 'Scale' - } +// Translation +messages: { + itemCount: '{count} {count, plural, one {item} other {items}}' } -``` - -## Internationalization Checklist -### Before Adding New Features - -- [ ] Plan translation structure for new components -- [ ] Identify reusable common translations -- [ ] Consider context and parameterization needs -- [ ] Plan for pluralization if needed - -### During Development - -- [ ] Use translation keys instead of hardcoded text -- [ ] Add proper TypeScript types for new translations -- [ ] Test with different languages if available -- [ ] Consider text length variations between languages - -### Before Release - -- [ ] Ensure all user-facing text is translatable -- [ ] Test language switching functionality -- [ ] Verify fallback translations work -- [ ] Check for missing translation keys -- [ ] Test form validation messages in different languages - -## Maintenance - -### Regular Translation Updates - -1. **Review translation completeness** for each supported language -2. **Update outdated translations** when features change -3. **Add missing translations** for new features -4. **Remove unused translation keys** to keep files clean - -### Translation File Organization - -Keep translation files organized and maintainable: - -```bash -# Good organization -src/i18n/locales/en/ -├── common.ts # Shared across app -├── navigation.ts # Navigation items -├── validation.ts # Form validation -├── notifications.ts # Success/error messages -├── features/ -│ ├── mcp-servers.ts -│ ├── deployment.ts -│ └── dashboard.ts +// Usage +t('messages.itemCount', { count: 1 }) // "1 item" +t('messages.itemCount', { count: 5 }) // "5 items" ``` -### Version Control Best Practices +## Quick Reference -- Keep translation files in version control -- Use meaningful commit messages for translation changes -- Consider separate PRs for translation updates -- Document translation conventions in your project +### Common Translation Keys -This comprehensive i18n setup ensures your DeployStack frontend can grow to support multiple languages while maintaining clean, organized, and maintainable translation files. +| Category | Key Example | Location | +|----------|------------|----------| +| Buttons | `buttons.save`, `buttons.cancel` | `common.ts` | +| Validation | `validation.required`, `validation.email` | `common.ts` | +| Status | `common.status.running` | `common.ts` | +| Errors | `{feature}.errors.{errorType}` | Feature file | +| Success | `{feature}.messages.success` | Feature file | +| Forms | `{feature}.form.{field}.label` | Feature file | diff --git a/docs/development/frontend/storage.mdx b/docs/development/frontend/storage.mdx index bf47da9..af4a662 100644 --- a/docs/development/frontend/storage.mdx +++ b/docs/development/frontend/storage.mdx @@ -114,11 +114,75 @@ console.log('All stored data:', allData) eventBus.clearAllState() ``` +## Storage Key Naming Convention + +The storage system follows strict naming conventions to ensure consistency and prevent conflicts across the application. + +### Naming Pattern + +``` +{feature}_{description} +``` + +All storage keys should: +- Use **snake_case** (lowercase with underscores) +- Start with the **feature name** or domain +- End with a **descriptive identifier** +- Be **specific and meaningful** + +### Examples + +```typescript +// Good storage key names +'selected_team_id' // Team selection +'user_preferences' // User preferences object +'dashboard_layout' // Dashboard configuration +'recent_searches' // Search history +'sidebar_collapsed' // UI state +'notification_settings' // Notification preferences +'selected_theme' // Theme selection +'last_visited_page' // Navigation tracking + +// Avoid these patterns +'data' // Too generic +'temp' // Not descriptive +'userPref' // Use snake_case, not camelCase +'TEAM_ID' // Use lowercase, not uppercase +'team-id' // Use underscores, not hyphens +``` + +### Storage Key Categories + +1. **UI State**: `{component}_{state}` + - `sidebar_collapsed` + - `modal_shown` + - `tab_selected` + +2. **User Preferences**: `{feature}_preferences` or `user_{setting}` + - `notification_preferences` + - `user_language` + - `user_timezone` + +3. **Selection State**: `selected_{entity}_id` + - `selected_team_id` + - `selected_project_id` + - `selected_workspace_id` + +4. **Cache/History**: `{feature}_history` or `recent_{items}` + - `search_history` + - `recent_searches` + - `visited_pages` + +5. **Feature Data**: `{feature}_{data_type}` + - `dashboard_layout` + - `form_draft` + - `wizard_state` + ## Adding New Storage Values -### Step 1: Add to Configuration (Optional) +### Step 1: Define Your Storage Key -For better organization, add your new storage key to the configuration: +Follow the naming convention and optionally add your key to the configuration for better organization: ```typescript // In /composables/useEventBus.ts @@ -126,9 +190,9 @@ const STORAGE_CONFIG = { prefix: 'deploystack_', keys: { SELECTED_TEAM_ID: 'selected_team_id', - SELECTED_THEME: 'selected_theme', // NEW - USER_DASHBOARD_LAYOUT: 'dashboard_layout', // NEW - RECENT_SEARCHES: 'recent_searches', // NEW + SELECTED_THEME: 'selected_theme', // NEW - follows pattern + USER_DASHBOARD_LAYOUT: 'dashboard_layout', // NEW - follows pattern + RECENT_SEARCHES: 'recent_searches', // NEW - follows pattern } } ``` @@ -252,67 +316,24 @@ const reorderPanels = (newOrder: string[]) => { ``` -### Example 3: Search History - -```typescript -// SearchComponent.vue - -``` - ## Best Practices -### 1. Use Descriptive Keys +### 1. Follow Naming Conventions + +Always use the established naming pattern `{feature}_{description}` with snake_case: ```typescript -// Good +// ✅ Good - follows convention eventBus.setState('selected_team_id', teamId) eventBus.setState('user_dashboard_layout', layout) eventBus.setState('notification_preferences', prefs) +eventBus.setState('sidebar_collapsed', true) -// Avoid -eventBus.setState('data', someData) -eventBus.setState('temp', tempValue) -eventBus.setState('x', value) +// ❌ Bad - violates convention +eventBus.setState('data', someData) // Too generic +eventBus.setState('selectedTeamId', teamId) // Wrong case (camelCase) +eventBus.setState('TEAM_ID', teamId) // Wrong case (uppercase) +eventBus.setState('team-id', teamId) // Wrong separator (hyphen) ``` ### 2. Provide Default Values @@ -422,45 +443,6 @@ The storage system uses `localStorage`, which is supported in all modern browser - **Storage Limits**: localStorage typically has a 5-10MB limit per domain - **Event Frequency**: Storage change events are emitted for every setState/clearState call -## Migration Guide - -### From Component State to Storage - -**Before:** -```typescript -// Component-level state -const selectedTeam = ref(null) - -onMounted(() => { - // Initialize from API or default - selectedTeam.value = await getDefaultTeam() -}) -``` - -**After:** -```typescript -// Storage-backed state -const selectedTeam = ref(null) - -onMounted(() => { - // Initialize from storage with fallback - const storedTeamId = eventBus.getState('selected_team_id') - if (storedTeamId) { - selectedTeam.value = await getTeamById(storedTeamId) - } else { - const defaultTeam = await getDefaultTeam() - selectedTeam.value = defaultTeam - eventBus.setState('selected_team_id', defaultTeam.id) - } -}) - -// Update storage when state changes -const selectTeam = (team: Team) => { - selectedTeam.value = team - eventBus.setState('selected_team_id', team.id) -} -``` - ## Related Documentation - **[Global Event Bus](/development/frontend/event-bus)** - Core event system that powers storage diff --git a/docs/development/frontend/ui-design-button-loading.mdx b/docs/development/frontend/ui-design-button-loading.mdx new file mode 100644 index 0000000..2281b7b --- /dev/null +++ b/docs/development/frontend/ui-design-button-loading.mdx @@ -0,0 +1,85 @@ +--- +title: Button with Loading States +description: Guide for using the enhanced Button component with built-in loading states. +sidebar: Button Loading +--- + +# Button with Loading States + +The Button component includes built-in loading state functionality for async operations. + +## Component Location + +``` +services/frontend/src/components/ui/button/ +├── Button.vue # Enhanced button component with loading states +└── index.ts # Button variants and exports +``` + +## Features + +- **Automatic spinner** with `Loader2` icon from lucide-vue-next +- **Auto-disable** during loading to prevent double submissions +- **Optional loading text** to display custom messages +- **Size-aware spinner** that scales with button size +- **Works with all variants** (default, destructive, outline, etc.) + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `loading` | `boolean` | `false` | Shows spinner and disables button | +| `loadingText` | `string` | `undefined` | Optional text during loading | +| `disabled` | `boolean` | `false` | Disable independent of loading | +| `variant` | `string` | `'default'` | Button style variant | +| `size` | `string` | `'default'` | Button size (sm, default, lg, icon) | + +## Usage Example + +```vue + + + +``` + +## Implementation Details + +The component automatically: +- Displays a spinning `Loader2` icon when `loading` is true +- Hides the original slot content during loading +- Shows `loadingText` alongside the spinner (if provided) +- Disables the button to prevent multiple clicks +- Adjusts spinner size based on button size prop + +For implementation details, see the source code at `services/frontend/src/components/ui/button/Button.vue`. + +## Related Documentation + +- [UI Design System](/development/frontend/ui-design-system) - Overall design patterns +- [Global Sonner Toast System](/development/frontend/ui-design-global-sonner) - Toast notifications diff --git a/docs/development/frontend/ui-design-system.mdx b/docs/development/frontend/ui-design-system.mdx index 9f99c41..bbd5a7a 100644 --- a/docs/development/frontend/ui-design-system.mdx +++ b/docs/development/frontend/ui-design-system.mdx @@ -16,6 +16,109 @@ This document establishes the official UI design patterns and component standard - **Performance**: Optimize for fast loading and smooth interactions - **Maintainability**: Write clean, reusable component code +## Color System + +### Primary Colors + +The DeployStack color palette uses teal as the primary brand color, ensuring WCAG compliance for accessibility. + +```css +/* Primary Brand Colors */ +--primary: #0f766e; /* teal-700 - WCAG AA compliant on white */ +--primary-hover: #115e59; /* teal-800 - WCAG AAA compliant on white */ +``` + +#### WCAG Compliance +- **Primary (teal-700)**: Contrast ratio 5.47:1 on white - ✅ AA Pass, ❌ AAA Fail +- **Primary-hover (teal-800)**: Contrast ratio 7.58:1 on white - ✅ AA Pass, ✅ AAA Pass + +This ensures our primary color meets accessibility standards, with enhanced contrast on hover states. + +### Text Colors +```css +/* Light Mode / Dark Mode */ +--text-primary: theme('colors.zinc.800') / theme('colors.zinc.100'); +--text-secondary: theme('colors.zinc.600') / theme('colors.zinc.400'); +``` + +### Background Colors +```css +/* Light Mode / Dark Mode */ +--bg-primary: white / theme('colors.zinc.900'); +--bg-secondary: theme('colors.zinc.50') / theme('colors.zinc.800'); +``` + +### Link Color Scheme + +DeployStack implements intelligent link styling with automatic teal coloring for text links while preserving button styling: + +#### Text Links +- **Light mode**: `text-teal-700` → `hover:text-teal-800` (WCAG AA → AAA on hover) +- **Dark mode**: `text-teal-400` → `hover:text-teal-300` (Already WCAG AAA compliant) +- **Underlines**: `decoration-teal-700/30` → `hover:decoration-teal-700/60` +- **Focus states**: `outline-teal-700` (light) / `outline-teal-400` (dark) + +#### Usage Examples +```html + +Learn more about us +External link + + +Sign Up +Login +``` + +### Color Usage Guidelines + +1. **Primary Actions**: Use `--primary` (teal-700) for primary buttons and key interactive elements +2. **Hover States**: Transition to `--primary-hover` (teal-800) for enhanced contrast +3. **Text Links**: Automatically styled with teal colors - no additional classes needed +4. **Button Links**: Include styling classes (`bg-*`, `rounded-*`) to maintain button appearance +5. **Focus States**: Ensure all interactive elements have visible focus indicators using the teal color scheme + +## Layout Design Patterns + +### Content Wrapper Pattern + +DeployStack follows a **mandatory content wrapper pattern** for all tabbed content and detail pages. This pattern ensures visual consistency and proper content hierarchy throughout the application. + +#### Design Requirements + +The content wrapper pattern is **required** for: +- Team management pages +- MCP server installation pages +- Settings and configuration pages +- Any page using tabbed content with `DsTabs` +- Detail views that need elevated content presentation + +#### Implementation + +Use the `ContentWrapper` component for all qualifying pages: + +```vue + + + +``` + +The wrapper provides: +- Gray background container (`bg-muted/50`) +- Responsive max-width constraints +- White card elevation with proper spacing +- Consistent vertical rhythm + +For complete implementation details, see the component source code at `services/frontend/src/components/ContentWrapper.vue`. + +#### Visual Hierarchy + +This pattern creates a three-tier visual hierarchy: +1. **Page background** - Default dashboard background +2. **Content container** - Gray muted background wrapper +3. **Content card** - White elevated card with content + +This hierarchy is a **design system requirement** and must be followed consistently across all applicable pages. + ## Data Tables For data table implementation, see the dedicated [Table Design System](/development/frontend/ui-design-system-table) guide. @@ -111,6 +214,20 @@ Use `AlertDialog` for forms in modals: ## Button Patterns +### Loading States +Buttons now include built-in loading state functionality. For comprehensive loading button documentation, see the [Button Loading States Guide](/development/frontend/ui-design-button-loading). + +```html + + +``` + ### Primary Actions ```html ``` ### Icon-Only Buttons ```html - ``` @@ -277,36 +400,4 @@ If you have an existing table using raw HTML elements, follow these steps: 4. **Update action menus** to use AlertDialog for destructive actions 5. **Ensure proper badge usage** for status indicators -### Example Migration - -**Before (deprecated):** -```html - - - - - - - - - - - -
Name
{{ item.name }}
-``` - -**After (preferred):** -```html - - - - Name - - - - - {{ item.name }} - - -
-``` +For detailed migration strategies and architectural considerations, see the [Frontend Architecture - Migration Guidelines](/development/frontend/architecture#migration-guidelines).