From 85fcc2289bc2610048bcdf4703d802b8740a1f50 Mon Sep 17 00:00:00 2001 From: ledgerpilot Date: Wed, 8 Apr 2026 00:40:52 -0700 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20address=20issue=20#97=20=E2=80=94=20?= =?UTF-8?q?Allow=20uploading=20of=20openapi=20spec=20for=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/resources/src/openapi/types.ts | 94 ++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/core/resources/src/openapi/types.ts diff --git a/packages/core/resources/src/openapi/types.ts b/packages/core/resources/src/openapi/types.ts new file mode 100644 index 000000000..2239c8395 --- /dev/null +++ b/packages/core/resources/src/openapi/types.ts @@ -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[]; +}; From ede65346dfca191aad58563ed27d267d33292f73 Mon Sep 17 00:00:00 2001 From: ledgerpilot Date: Wed, 8 Apr 2026 00:40:53 -0700 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20address=20issue=20#97=20=E2=80=94=20?= =?UTF-8?q?Allow=20uploading=20of=20openapi=20spec=20for=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/resources/src/openapi/parser.ts | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 packages/core/resources/src/openapi/parser.ts diff --git a/packages/core/resources/src/openapi/parser.ts b/packages/core/resources/src/openapi/parser.ts new file mode 100644 index 000000000..f6d25e73f --- /dev/null +++ b/packages/core/resources/src/openapi/parser.ts @@ -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}`] }; + } +} From 9fe2e7538998486ab98c967d046d3c68fe4069cd Mon Sep 17 00:00:00 2001 From: ledgerpilot Date: Wed, 8 Apr 2026 00:40:54 -0700 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20address=20issue=20#97=20=E2=80=94=20?= =?UTF-8?q?Allow=20uploading=20of=20openapi=20spec=20for=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/services/resourceSpecService.ts | 61 ++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 apps/api/src/services/resourceSpecService.ts diff --git a/apps/api/src/services/resourceSpecService.ts b/apps/api/src/services/resourceSpecService.ts new file mode 100644 index 000000000..878340553 --- /dev/null +++ b/apps/api/src/services/resourceSpecService.ts @@ -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 { + 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(); From 797bf0ed1c9fb8b1c870fc41525331d90c8fe094 Mon Sep 17 00:00:00 2001 From: ledgerpilot Date: Wed, 8 Apr 2026 00:40:55 -0700 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20address=20issue=20#97=20=E2=80=94=20?= =?UTF-8?q?Allow=20uploading=20of=20openapi=20spec=20for=20resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/api/src/routes/resources/spec.ts | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 apps/api/src/routes/resources/spec.ts diff --git a/apps/api/src/routes/resources/spec.ts b/apps/api/src/routes/resources/spec.ts new file mode 100644 index 000000000..5d06d1518 --- /dev/null +++ b/apps/api/src/routes/resources/spec.ts @@ -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 +*/