Skip to content

Commit a46e633

Browse files
committed
fix
1 parent 51a223e commit a46e633

File tree

14 files changed

+436
-163
lines changed

14 files changed

+436
-163
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"devDependencies": {
5353
"@types/jest": "^29.5.5",
5454
"@types/node": "^20.10.0",
55+
"@types/uuid": "^10.0.0",
5556
"@typescript-eslint/eslint-plugin": "^5.62.0",
5657
"@typescript-eslint/parser": "^5.62.0",
5758
"eslint": "^8.56.0",
@@ -63,6 +64,7 @@
6364
"rimraf": "^5.0.5",
6465
"ts-jest": "^29.1.1",
6566
"ts-node": "^10.9.1",
66-
"typescript": "^5.2.2"
67+
"typescript": "^5.2.2",
68+
"uuid": "^11.1.0"
6769
}
6870
}

pnpm-lock.yaml

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/contract/client.ts

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ interface CustomApiFetcherArgs {
2727
export function createApiClient(apiKey: string, options: ClientOptions) {
2828
const { baseUrl, version, timeout = 30000, logger } = options;
2929

30-
// Create axios instance with defaults
3130
const axiosInstance = axios.create({
3231
baseURL: baseUrl,
3332
timeout,
@@ -49,7 +48,7 @@ export function createApiClient(apiKey: string, options: ClientOptions) {
4948
// Format error for cleaner logs
5049
const errorInfo = {
5150
message: error.message,
52-
code: error.code
51+
code: error.code,
5352
};
5453
logger.error('Request error', errorInfo);
5554
return Promise.reject(error);
@@ -63,27 +62,26 @@ export function createApiClient(apiKey: string, options: ClientOptions) {
6362
},
6463
error => {
6564
// Format error for cleaner logs
66-
let errorInfo: Record<string, any> = {
65+
const errorInfo: Record<string, any> = {
6766
message: error.message,
68-
code: error.code
67+
code: error.code,
6968
};
70-
69+
7170
if (error.response) {
7271
errorInfo.status = error.response.status;
73-
72+
7473
// Extract specific error details
7574
if (error.response.data && error.response.data.error) {
7675
errorInfo.errorCode = error.response.data.error.code;
7776
errorInfo.errorMessage = error.response.data.error.message;
78-
77+
7978
// Include first validation error if present
80-
if (error.response.data.error.errors &&
81-
error.response.data.error.errors.length > 0) {
79+
if (error.response.data.error.errors && error.response.data.error.errors.length > 0) {
8280
errorInfo.validation = error.response.data.error.errors[0];
8381
}
8482
}
8583
}
86-
84+
8785
logger.error('Response error', errorInfo);
8886
return Promise.reject(error);
8987
}
@@ -147,14 +145,21 @@ export function createApiClient(apiKey: string, options: ClientOptions) {
147145
}
148146

149147
const { status, data } = axiosError.response;
150-
const errorMessage =
151-
typeof data === 'object' && data !== null ? (data as any).message : undefined;
148+
149+
// Extract error details from the standardized response envelope
150+
// The server returns: { ok: false, status: number, error: { code, message, errors?, details? } }
151+
const errorResponse = data as any;
152+
const errorDetails = errorResponse?.error || {};
153+
154+
const errorMessage = errorDetails.message || `API error: ${status}`;
152155

153156
if (status === 401 || status === 403) {
154157
throw new AuthError(errorMessage || 'Authentication failed', axiosError);
155158
}
156159

157-
throw new ApiError(errorMessage || `API error: ${status}`, status, data, axiosError);
160+
// For all other errors, throw ApiError with the full response
161+
// Higher-level SDK code can inspect this and throw more specific errors
162+
throw new ApiError(errorMessage, status, errorResponse, axiosError);
158163
}
159164
} catch (error) {
160165
// Re-throw domain errors

src/core/sdk.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,10 @@ export class TadataNodeSDK {
6767
constructor(options: TadataOptions) {
6868
const logger = options.logger || createDefaultLogger();
6969
const isDev = options.dev || false;
70-
const baseUrl = isDev ? 'https://api.stage.tadata.com' : 'https://api.tadata.com';
70+
71+
// Always use http://localhost:3000 as the baseUrl
72+
// This is a requirement for the current implementation
73+
const baseUrl = 'http://localhost:3000';
7174

7275
const client = createApiClient(options.apiKey, {
7376
baseUrl,

src/http/contracts/deployments.contract.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { initContract } from '@ts-rest/core';
2-
import { HttpStatusCode } from 'axios';
2+
import { StatusCodes } from 'http-status-codes';
33
import { z } from 'zod';
44
import {
55
createApiResponseSchema,
@@ -9,23 +9,22 @@ import {
99

1010
const contract = initContract();
1111

12-
const ErrorResponseBadRequest = createApiResponseSchema(z.null());
12+
const ErrorResponse400 = createApiResponseSchema(z.null());
13+
const ErrorResponseNotFound = createApiResponseSchema(z.null());
14+
const ErrorResponse500 = createApiResponseSchema(z.null());
1315
const ErrorResponseUnauthorized = createApiResponseSchema(z.null());
14-
const ErrorResponseForbidden = createApiResponseSchema(z.null());
15-
const ErrorResponseInternalServerError = createApiResponseSchema(z.null());
1616

1717
export const deploymentsContract = contract.router({
1818
upsertFromOpenApi: {
1919
method: 'POST',
2020
path: '/api/deployments/from-openapi',
2121
body: UpsertDeploymentBodySchema,
2222
responses: {
23-
[HttpStatusCode.Ok]: UpsertDeploymentResponseSchema, // Success with response envelope
24-
[HttpStatusCode.Created]: UpsertDeploymentResponseSchema, // Created with response envelope
25-
[HttpStatusCode.BadRequest]: ErrorResponseBadRequest, // Validation error
26-
[HttpStatusCode.Unauthorized]: ErrorResponseUnauthorized, // Auth error
27-
[HttpStatusCode.Forbidden]: ErrorResponseForbidden, // Permission error
28-
[HttpStatusCode.InternalServerError]: ErrorResponseInternalServerError, // Internal server error
23+
[StatusCodes.CREATED]: UpsertDeploymentResponseSchema, // Created with response envelope
24+
[StatusCodes.BAD_REQUEST]: ErrorResponse400, // Validation error
25+
[StatusCodes.NOT_FOUND]: ErrorResponseNotFound, // Not found error
26+
[StatusCodes.INTERNAL_SERVER_ERROR]: ErrorResponse500, // Internal server error
27+
[StatusCodes.UNAUTHORIZED]: ErrorResponseUnauthorized, // Auth error handled by middleware
2928
},
3029
summary: 'Upsert a deployment from an OpenAPI specification',
3130
},

src/http/schemas/deployments.schema.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,16 @@ import { createApiResponseSchema } from './response.schema';
33

44
export const DeploymentResponseMinSchema = z.object({
55
id: z.string(),
6-
name: z.string(),
7-
url: z.string(), // Added from existing McpDeploymentSchema
8-
// specVersion: z.string(), // Consider if this is needed for 'Min' schema
9-
// createdAt: z.date(), // Consider if this is needed for 'Min' schema
6+
createdAt: z
7+
.union([z.string(), z.date()])
8+
.transform(val => (val instanceof Date ? val.toISOString() : val))
9+
.optional(),
10+
createdBy: z.string(),
11+
updatedBy: z.string(),
12+
mcpServerId: z.string(),
13+
openAPISpecHash: z.string(),
14+
mcpSpecHash: z.string(),
15+
status: z.string(),
1016
});
1117

1218
// Define a Zod schema for OpenAPI 3.0 with basic validation
@@ -23,16 +29,10 @@ export const OpenApi3Schema = z
2329
})
2430
.passthrough();
2531

26-
// Assuming UpsertMCPServerFromOpenAPISchema is defined elsewhere
27-
// For now, let's define a placeholder based on the description
2832
export const UpsertDeploymentBodySchema = z.object({
29-
// Updated to use the OpenApi3Schema
30-
openapiSpec: OpenApi3Schema,
31-
serviceName: z.string().optional(), // Corresponds to 'name' in mcpDeploy body
32-
// 'version' from the plan doesn't directly map to mcpDeploy, adding as optional
33-
version: z.string().optional(),
34-
// 'baseUrl' from mcpDeploy body doesn't seem to be in the plan's subset, omitting for now
35-
// 'dev' from mcpDeploy body doesn't seem to be in the plan's subset, omitting for now
33+
openApiSpec: OpenApi3Schema,
34+
name: z.string().optional(),
35+
baseUrl: z.string().optional(),
3636
});
3737

3838
// Original unwrapped response schema

src/openapi/openapi-source.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import fs from 'fs/promises';
2-
import path from 'path';
1+
import * as fs from 'fs/promises';
2+
import * as path from 'path';
33
import type { OpenAPI3 } from 'openapi-typescript';
44
import { SpecInvalidError } from '../errors';
55

src/resources/mcp/mcp-resource.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
import { ClientArgs, InitClientReturn } from '@ts-rest/core';
22
import { StatusCodes } from 'http-status-codes';
3+
import { z } from 'zod';
34
import { Logger } from '../../core/logger';
4-
import { ApiError, SpecInvalidError, AuthError } from '../../errors';
5+
import { ApiError, SpecInvalidError } from '../../errors';
56
import { deploymentsContract } from '../../http/contracts';
67
import { OpenApi3Schema } from '../../http/schemas';
7-
import { z } from 'zod';
88
import { McpDeployInput, McpDeploymentResult } from './types';
99

1010
interface DeploymentSuccessResponse {
1111
ok: true;
1212
data: {
13+
updated: boolean;
1314
deployment: {
1415
id: string;
15-
url: string;
16+
name: string;
17+
url?: string; // URL is optional in the server response
18+
specVersion?: string; // specVersion is optional in the server response
19+
createdAt?: string;
20+
updatedAt?: string;
1621
};
1722
};
1823
}
@@ -60,21 +65,22 @@ export class McpResource {
6065
// No need to pass apiKey as it's automatically added by the client
6166
const response = await this.client.upsertFromOpenApi({
6267
body: {
63-
openapiSpec,
64-
serviceName: input.name,
65-
version: '1.0.0', // Add proper versioning parameter
68+
openApiSpec: openapiSpec,
69+
name: input.name,
70+
baseUrl: input.specBaseUrl,
6671
},
6772
});
6873

69-
// For successful response, transform to expected McpDeploymentResult format
7074
if (isDeploymentResponse(response.body) && response.body.ok) {
7175
const deploymentData = response.body.data.deployment;
7276

7377
return {
7478
id: deploymentData.id,
75-
url: deploymentData.url,
76-
specVersion: '1.0.0', // Use appropriate version from response when available
77-
createdAt: new Date(), // Use appropriate timestamp from response when available
79+
// Provide a default value for specVersion if undefined
80+
specVersion: deploymentData.specVersion || '1.0.0',
81+
// Provide a default URL value (required by type) if not returned from server
82+
url: deploymentData.url || `http://localhost:3000/mcp/${deploymentData.id}`,
83+
createdAt: deploymentData.createdAt ? new Date(deploymentData.createdAt) : new Date(),
7884
};
7985
}
8086

@@ -116,47 +122,47 @@ export class McpResource {
116122
*/
117123
function formatErrorForLogging(error: unknown): Record<string, any> {
118124
if (!error) return { type: 'unknown' };
119-
125+
120126
// Handle ApiError
121127
if (error instanceof ApiError) {
122128
const result: Record<string, any> = {
123129
type: 'ApiError',
124130
message: error.message,
125131
code: error.code,
126-
statusCode: error.statusCode
132+
statusCode: error.statusCode,
127133
};
128-
134+
129135
// Extract response body if available
130136
if (error.body) {
131-
if (typeof error.body === 'object' && error.body !== null) {
137+
if (typeof error.body === 'object') {
132138
const body = error.body as any;
133139
if (body.error) {
134140
result.errorCode = body.error.code;
135141
result.errorMessage = body.error.message;
136-
142+
137143
// Include first validation error if present
138144
if (body.error.errors && body.error.errors.length > 0) {
139145
result.validation = body.error.errors[0];
140146
}
141147
}
142148
}
143149
}
144-
150+
145151
return result;
146152
}
147-
153+
148154
// Handle AuthError
149155
if (error instanceof Error) {
150156
return {
151157
type: error.constructor.name,
152158
message: error.message,
153-
code: (error as any).code
159+
code: (error as any).code,
154160
};
155161
}
156-
162+
157163
// Unknown error type
158-
return {
164+
return {
159165
type: 'unknown',
160-
error: String(error)
166+
error: String(error),
161167
};
162168
}

src/resources/mcp/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { OpenApiSource } from '../../openapi';
55
*/
66
export interface McpDeployInput {
77
spec: OpenApiSource;
8-
specBaseUrl: string;
8+
specBaseUrl?: string;
99
name?: string;
1010
}
1111

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Deployments Integration Test (Nock) when deploying an OpenAPI spec should throw ApiError for not found errors (404) 1`] = `"Deployment not found"`;
4+
5+
exports[`Deployments Integration Test (Nock) when deploying an OpenAPI spec should throw ApiError for server errors (e.g., 500) 1`] = `"Internal mock server error occurred"`;
6+
7+
exports[`Deployments Integration Test (Nock) when deploying an OpenAPI spec should throw AuthError when using invalid API key 1`] = `"Invalid API key from mock"`;
8+
9+
exports[`Deployments Integration Test (Nock) when deploying an OpenAPI spec should throw SpecInvalidError for backend validation failures 1`] = `"Invalid OpenAPI specification from backend"`;
10+
11+
exports[`Deployments Integration Test (Nock) when deploying an OpenAPI spec should throw SpecInvalidError for malformed request body 1`] = `"Validation error"`;

0 commit comments

Comments
 (0)