Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions apps/api/src/routes/resources/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Request, Response } from 'express'; // Assuming Express.js framework for API routes
import { resourceSpecService } from '../../services/resourceSpecService';

/**
* API route handler for submitting an OpenAPI specification for a resource.
* This endpoint accepts a resource ID and the raw OpenAPI spec content (JSON or YAML string).
* It delegates the parsing and processing to the `resourceSpecService`.
*
* @param req The Express request object.
* Expected `req.body`: `{ resourceId: string, specContent: string }`
* @param res The Express response object.
*/
export async function submitResourceOpenAPISpec(req: Request, res: Response) {
const { resourceId, specContent } = req.body;

if (!resourceId || typeof resourceId !== 'string' || resourceId.trim() === '') {
return res.status(400).json({ success: false, message: 'Invalid or missing "resourceId" in request body.' });
}
if (!specContent || typeof specContent !== 'string' || specContent.trim() === '') {
return res.status(400).json({ success: false, message: 'Invalid or missing "specContent" (OpenAPI spec string) in request body.' });
}

try {
const result = await resourceSpecService.processAndStoreOpenAPISpec(resourceId, specContent);

if (result.success) {
// Upon successful processing, you might return:
// - A confirmation message.
// - The ID of the updated resource.
// - A URL or endpoint for the user to test their newly defined schema.
// - Optionally, a subset of the parsed schema for immediate feedback.
return res.status(200).json({
success: true,
message: `OpenAPI spec successfully processed and linked to resource "${resourceId}".`,
parsedSchemaSummary: { // Provide a summary rather than the full schema for response brevity
title: result.schema.title,
version: result.schema.version,
pathsCount: Object.keys(result.schema.paths).length,
},
// For the "test the schema" part of the issue, a dedicated endpoint would be needed:
// testSchemaUrl: `/api/resources/${resourceId}/schema/test`,
});
} else {
// If parsing or processing failed, return detailed errors.
return res.status(400).json({
success: false,
message: 'Failed to parse or process the provided OpenAPI spec.',
errors: result.errors,
});
}
} catch (error: any) {
// Catch any unexpected server-side errors
console.error(`[API Error] Failed to submit OpenAPI spec for resource "${resourceId}":`, error);
return res.status(500).json({
success: false,
message: 'An internal server error occurred while processing your OpenAPI spec.',
error: error.message,
});
}
}

// Example of how this route could be registered in an Express application:
/*
import express from 'express';
const router = express.Router();

// Define a POST endpoint for submitting OpenAPI specs
router.post('/resources/:resourceId/spec', submitResourceOpenAPISpec);

// In your main app file (e.g., app.ts or server.ts):
// app.use('/api', router); // Mount the router under the /api path
*/
61 changes: 61 additions & 0 deletions apps/api/src/services/resourceSpecService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { parseOpenAPISpec } from '../../../packages/core/resources/src/openapi/parser';
import { ParsedOpenAPISchema, ParsedOpenAPIResult } from '../../../packages/core/resources/src/openapi/types';

/**
* Service to handle the processing and storage of resource OpenAPI specifications.
* This class encapsulates the business logic for managing OpenAPI specs for resources.
*/
export class ResourceSpecService {
/**
* Processes an OpenAPI spec string, parses it, and conceptually stores it.
* In a real application, this method would interact with a database or a
* resource management system to persist the `schema` object associated
* with a specific `resourceId`.
*
* @param resourceId The ID of the resource this spec belongs to.
* @param specContent The raw OpenAPI spec content (JSON or YAML string).
* @returns A Promise resolving to ParsedOpenAPIResult, indicating success/failure
* and containing the parsed schema or a list of errors.
*/
public async processAndStoreOpenAPISpec(resourceId: string, specContent: string): Promise<ParsedOpenAPIResult> {
const parseResult = parseOpenAPISpec(specContent);

if (!parseResult.success) {
console.error(`Failed to parse OpenAPI spec for resource ${resourceId}:`, parseResult.errors);
return parseResult;
}

const { schema } = parseResult;

// --- Conceptual Storage Logic ---
// In a production application, this is where you would:
// 1. Validate the parsed 'schema' further against application-specific rules.
// 2. Interact with your database to save or update the 'schema' object
// associated with 'resourceId'. This might involve:
// `await db.resourceSchemas.upsert({ resourceId, schemaJson: JSON.stringify(schema) });`
// 3. Potentially generate derived artifacts like client SDKs or API documentation.
console.log(`Successfully parsed OpenAPI spec for resource "${resourceId}". Schema details:`);
console.log(` Title: ${schema.title}`);
console.log(` Version: ${schema.version}`);
console.log(` Paths defined: ${Object.keys(schema.paths).length}`);

// Placeholder for actual storage:
// try {
// await this.resourceRepository.updateResourceSchema(resourceId, schema);
// console.log(`OpenAPI spec for resource ${resourceId} successfully stored.`);
// } catch (dbError: any) {
// console.error(`Failed to store OpenAPI spec for resource ${resourceId}:`, dbError);
// return { success: false, errors: [`Failed to store schema: ${dbError.message}`] };
// }
// --- End Conceptual Storage Logic ---

// The "testing the schema" aspect mentioned in the issue would involve
// another method in this service, perhaps `testResourceSchema(resourceId, query)`
// which would use the stored `schema` to validate and potentially proxy a test query.

return { success: true, schema };
}
}

// Export a singleton instance of the service
export const resourceSpecService = new ResourceSpecService();
77 changes: 77 additions & 0 deletions packages/core/resources/src/openapi/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Assuming 'yaml' and a robust OpenAPI parser like 'swagger-parser' or 'openapi-parser'
// would be installed as dependencies. For this example, we use a basic approach.
import { parse as parseYaml } from 'yaml';
import { ParsedOpenAPISchema, ParsedOpenAPIResult } from './types';

/**
* Parses an OpenAPI spec string (JSON or YAML) into a structured object.
* This is a simplified parser for demonstration purposes. In a production environment,
* a robust OpenAPI parser library (e.g., '@apidevtools/swagger-parser' or 'oas-parser')
* should be used for full validation, dereferencing, and normalization.
*
* @param specString The raw OpenAPI spec content (JSON or YAML).
* @returns A ParsedOpenAPIResult indicating success/failure and the parsed schema or errors.
*/
export function parseOpenAPISpec(specString: string): ParsedOpenAPIResult {
try {
let parsed: any;
let isYaml = false;

// Attempt to parse as JSON first
try {
parsed = JSON.parse(specString);
} catch (jsonError: any) {
// If JSON parsing fails, try YAML
try {
parsed = parseYaml(specString);
isYaml = true;
} catch (yamlError: any) {
return {
success: false,
errors: [
'Invalid OpenAPI spec format: Content is neither valid JSON nor YAML.',
`JSON parsing error: ${jsonError.message}`,
`YAML parsing error: ${yamlError.message}`,
],
};
}
}

// Basic structural validation for OpenAPI 3.x
if (!parsed || typeof parsed !== 'object') {
return { success: false, errors: ['Parsed content is not a valid object.'] };
}
if (!parsed.openapi || typeof parsed.openapi !== 'string' || !parsed.openapi.startsWith('3.')) {
return { success: false, errors: [`Invalid or unsupported OpenAPI version. Expected '3.x.x', got '${parsed.openapi || 'none'}'`] };
}
if (!parsed.paths || typeof parsed.paths !== 'object' || Object.keys(parsed.paths).length === 0) {
return { success: false, errors: ['OpenAPI spec must contain a "paths" object with at least one path.'] };
}
if (!parsed.info || typeof parsed.info !== 'object' || !parsed.info.title || !parsed.info.version) {
return { success: false, errors: ['OpenAPI spec must contain "info" object with "title" and "version".'] };
}

// Extract the relevant parts of the schema for internal representation
const schema: ParsedOpenAPISchema = {
version: parsed.openapi,
title: parsed.info.title,
description: parsed.info.description,
servers: parsed.servers,
paths: parsed.paths,
components: parsed.components,
security: parsed.security,
tags: parsed.tags,
externalDocs: parsed.externalDocs,
};

// In a real implementation, you would use a dedicated OpenAPI validator
// here to ensure the spec is fully compliant and well-formed.
// E.g., const validationErrors = await SwaggerParser.validate(parsed);
// If validationErrors.length > 0, return { success: false, errors: validationErrors };

return { success: true, schema };
} catch (e: any) {
// Catch any unexpected errors during the process
return { success: false, errors: [`An unexpected error occurred during OpenAPI spec parsing: ${e.message}`] };
}
}
94 changes: 94 additions & 0 deletions packages/core/resources/src/openapi/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Defines the internal structure for a parsed OpenAPI Operation.
* This is a simplified representation to focus on relevant fields for resource querying.
*/
export interface ParsedOpenAPIOperation {
operationId?: string;
summary?: string;
description?: string;
parameters?: Array<{
name: string;
in: 'query' | 'header' | 'path' | 'cookie';
description?: string;
required?: boolean;
schema: any; // Simplified schema definition (e.g., JSDoc for OpenAPI Schema Object)
example?: any;
examples?: { [key: string]: { value: any; summary?: string; description?: string } };
}>;
requestBody?: {
description?: string;
required?: boolean;
content: {
[mediaType: string]: {
schema: any; // Simplified schema definition
example?: any;
examples?: { [key: string]: { value: any; summary?: string; description?: string } };
};
};
};
responses: {
[statusCode: string]: {
description: string;
content?: {
[mediaType: string]: {
schema: any; // Simplified schema definition
example?: any;
examples?: { [key: string]: { value: any; summary?: string; description?: string } };
};
};
};
};
}

/**
* Defines the internal structure for a parsed OpenAPI Path Item (e.g., /users/{id}).
*/
export interface ParsedOpenAPIPathItem {
[method: string]: ParsedOpenAPIOperation; // e.g., 'get', 'post', 'put', 'delete'
}

/**
* Defines the internal structure for a parsed OpenAPI Specification,
* extracting key information needed for defining and querying resources.
*/
export interface ParsedOpenAPISchema {
version: string; // e.g., '3.0.0' or '3.1.0'
title?: string;
description?: string;
servers?: Array<{ url: string; description?: string }>;
paths: {
[path: string]: ParsedOpenAPIPathItem;
};
components?: {
schemas?: {
[name: string]: any; // Reusable schema definitions
};
securitySchemes?: {
[name: string]: any; // Authentication mechanisms
};
parameters?: {
[name: string]: any; // Reusable parameter definitions
};
requestBodies?: {
[name: string]: any; // Reusable request body definitions
};
responses?: {
[name: string]: any; // Reusable response definitions
};
// ... other components as needed
};
security?: Array<{ [key: string]: string[] }>; // Global security requirements
tags?: Array<{ name: string; description?: string }>; // Tags used for operations
externalDocs?: { url: string; description?: string }; // External documentation
}

/**
* Represents the result of parsing an OpenAPI spec string.
*/
export type ParsedOpenAPIResult = {
success: true;
schema: ParsedOpenAPISchema;
} | {
success: false;
errors: string[];
};