Skip to content

Commit 98cc8c3

Browse files
feat(api): implement API protocol Phase 1 - contracts, responses, and errors
- Implement standardized API response wrappers (success/error with metadata) - Add request contract schemas (Create, Update, Query, Delete, Batch) - Create comprehensive error handling system with standard error codes - Add 30+ unit tests for API components (all passing) - Export API components from kernel index - Create detailed implementation plan for remaining API protocol phases Phase 1 Complete: API Contracts & Response Schemas - ApiResponse<T> with success/error/meta - Standard request schemas for CRUD operations - Error hierarchy with HTTP status codes - Request validation functions - 90/90 tests passing Co-authored-by: xuyushun441-sys <255036401+xuyushun441-sys@users.noreply.github.com>
1 parent cd8f6a7 commit 98cc8c3

9 files changed

Lines changed: 1176 additions & 0 deletions

File tree

API_PROTOCOL_IMPLEMENTATION_PLAN.md

Lines changed: 465 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Standard API Request Contracts
3+
*
4+
* Implements request schemas according to @objectstack/spec/api
5+
*/
6+
7+
export type RecordData = Record<string, any>;
8+
9+
/**
10+
* Create Request
11+
*/
12+
export interface CreateRequest {
13+
data: RecordData;
14+
}
15+
16+
/**
17+
* Update Request
18+
*/
19+
export interface UpdateRequest {
20+
id: string;
21+
data: RecordData;
22+
}
23+
24+
/**
25+
* Query Filter
26+
*/
27+
export interface QueryFilter {
28+
field: string;
29+
operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'contains' | 'startsWith' | 'endsWith';
30+
value: any;
31+
}
32+
33+
/**
34+
* Query Request
35+
*/
36+
export interface QueryRequest {
37+
filters?: QueryFilter[];
38+
fields?: string[];
39+
sort?: Array<{ field: string; order: 'asc' | 'desc' }>;
40+
limit?: number;
41+
offset?: number;
42+
}
43+
44+
/**
45+
* Delete Request
46+
*/
47+
export interface DeleteRequest {
48+
id: string;
49+
}
50+
51+
/**
52+
* Batch Request
53+
*/
54+
export interface BatchRequest<T = any> {
55+
operations: Array<{
56+
type: 'create' | 'update' | 'delete';
57+
data?: T;
58+
id?: string;
59+
}>;
60+
}
61+
62+
/**
63+
* Validate Create Request
64+
*/
65+
export function validateCreateRequest(request: any): request is CreateRequest {
66+
return !!request && typeof request === 'object' && 'data' in request && typeof request.data === 'object' && request.data !== null;
67+
}
68+
69+
/**
70+
* Validate Update Request
71+
*/
72+
export function validateUpdateRequest(request: any): request is UpdateRequest {
73+
return (
74+
!!request &&
75+
typeof request === 'object' &&
76+
'id' in request &&
77+
typeof request.id === 'string' &&
78+
'data' in request &&
79+
typeof request.data === 'object' &&
80+
request.data !== null
81+
);
82+
}
83+
84+
/**
85+
* Validate Query Request
86+
*/
87+
export function validateQueryRequest(request: any): request is QueryRequest {
88+
if (!request || typeof request !== 'object') {
89+
return false;
90+
}
91+
92+
// All fields are optional
93+
if (request.filters && !Array.isArray(request.filters)) {
94+
return false;
95+
}
96+
97+
if (request.fields && !Array.isArray(request.fields)) {
98+
return false;
99+
}
100+
101+
if (request.sort && !Array.isArray(request.sort)) {
102+
return false;
103+
}
104+
105+
return true;
106+
}
107+
108+
/**
109+
* Validate Delete Request
110+
*/
111+
export function validateDeleteRequest(request: any): request is DeleteRequest {
112+
return !!request && typeof request === 'object' && 'id' in request && typeof request.id === 'string';
113+
}

packages/kernel/src/api/errors.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
* API Error Handling
3+
*
4+
* Implements standardized error codes and error classes
5+
*/
6+
7+
export enum ApiErrorCode {
8+
// Client Errors (4xx)
9+
BAD_REQUEST = 'BAD_REQUEST',
10+
UNAUTHORIZED = 'UNAUTHORIZED',
11+
FORBIDDEN = 'FORBIDDEN',
12+
NOT_FOUND = 'NOT_FOUND',
13+
CONFLICT = 'CONFLICT',
14+
VALIDATION_ERROR = 'VALIDATION_ERROR',
15+
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
16+
17+
// Server Errors (5xx)
18+
INTERNAL_ERROR = 'INTERNAL_ERROR',
19+
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
20+
TIMEOUT = 'TIMEOUT',
21+
22+
// Business Logic Errors
23+
DUPLICATE_ENTRY = 'DUPLICATE_ENTRY',
24+
INVALID_STATE = 'INVALID_STATE',
25+
PERMISSION_DENIED = 'PERMISSION_DENIED',
26+
}
27+
28+
/**
29+
* Base API Error class
30+
*/
31+
export class ApiError extends Error {
32+
public readonly code: string;
33+
public readonly statusCode: number;
34+
public readonly details?: any;
35+
36+
constructor(code: string, message: string, statusCode: number = 500, details?: any) {
37+
super(message);
38+
this.name = 'ApiError';
39+
this.code = code;
40+
this.statusCode = statusCode;
41+
this.details = details;
42+
43+
// Maintains proper stack trace for where error was thrown
44+
Error.captureStackTrace(this, this.constructor);
45+
}
46+
}
47+
48+
/**
49+
* Bad Request Error (400)
50+
*/
51+
export class BadRequestError extends ApiError {
52+
constructor(message: string = 'Bad Request', details?: any) {
53+
super(ApiErrorCode.BAD_REQUEST, message, 400, details);
54+
this.name = 'BadRequestError';
55+
}
56+
}
57+
58+
/**
59+
* Unauthorized Error (401)
60+
*/
61+
export class UnauthorizedError extends ApiError {
62+
constructor(message: string = 'Unauthorized', details?: any) {
63+
super(ApiErrorCode.UNAUTHORIZED, message, 401, details);
64+
this.name = 'UnauthorizedError';
65+
}
66+
}
67+
68+
/**
69+
* Forbidden Error (403)
70+
*/
71+
export class ForbiddenError extends ApiError {
72+
constructor(message: string = 'Forbidden', details?: any) {
73+
super(ApiErrorCode.FORBIDDEN, message, 403, details);
74+
this.name = 'ForbiddenError';
75+
}
76+
}
77+
78+
/**
79+
* Not Found Error (404)
80+
*/
81+
export class NotFoundError extends ApiError {
82+
constructor(message: string = 'Resource not found', details?: any) {
83+
super(ApiErrorCode.NOT_FOUND, message, 404, details);
84+
this.name = 'NotFoundError';
85+
}
86+
}
87+
88+
/**
89+
* Conflict Error (409)
90+
*/
91+
export class ConflictError extends ApiError {
92+
constructor(message: string = 'Resource conflict', details?: any) {
93+
super(ApiErrorCode.CONFLICT, message, 409, details);
94+
this.name = 'ConflictError';
95+
}
96+
}
97+
98+
/**
99+
* Validation Error (422)
100+
*/
101+
export class ValidationError extends ApiError {
102+
constructor(message: string = 'Validation failed', details?: any) {
103+
super(ApiErrorCode.VALIDATION_ERROR, message, 422, details);
104+
this.name = 'ValidationError';
105+
}
106+
}
107+
108+
/**
109+
* Rate Limit Exceeded Error (429)
110+
*/
111+
export class RateLimitError extends ApiError {
112+
constructor(message: string = 'Rate limit exceeded', details?: any) {
113+
super(ApiErrorCode.RATE_LIMIT_EXCEEDED, message, 429, details);
114+
this.name = 'RateLimitError';
115+
}
116+
}
117+
118+
/**
119+
* Internal Error (500)
120+
*/
121+
export class InternalError extends ApiError {
122+
constructor(message: string = 'Internal server error', details?: any) {
123+
super(ApiErrorCode.INTERNAL_ERROR, message, 500, details);
124+
this.name = 'InternalError';
125+
}
126+
}
127+
128+
/**
129+
* Service Unavailable Error (503)
130+
*/
131+
export class ServiceUnavailableError extends ApiError {
132+
constructor(message: string = 'Service unavailable', details?: any) {
133+
super(ApiErrorCode.SERVICE_UNAVAILABLE, message, 503, details);
134+
this.name = 'ServiceUnavailableError';
135+
}
136+
}
137+
138+
/**
139+
* Convert error to API error
140+
*/
141+
export function toApiError(error: any): ApiError {
142+
if (error instanceof ApiError) {
143+
return error;
144+
}
145+
146+
// Handle common error types
147+
if (error.name === 'ValidationError') {
148+
return new ValidationError(error.message, error.details);
149+
}
150+
151+
if (error.name === 'NotFoundError') {
152+
return new NotFoundError(error.message, error.details);
153+
}
154+
155+
// Default to internal error
156+
return new InternalError(error.message || 'An unexpected error occurred', {
157+
originalError: error.message,
158+
stack: error.stack,
159+
});
160+
}

packages/kernel/src/api/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* API Protocol Exports
3+
*
4+
* Main entry point for API-related functionality
5+
*/
6+
7+
// Response handling
8+
export {
9+
ApiResponseMeta,
10+
ApiResponse,
11+
createSuccessResponse,
12+
createErrorResponse,
13+
wrapApiResponse,
14+
} from './response';
15+
16+
// Request contracts
17+
export * from './contracts';
18+
19+
// Error handling
20+
export {
21+
ApiErrorCode,
22+
ApiError,
23+
BadRequestError,
24+
UnauthorizedError,
25+
ForbiddenError,
26+
NotFoundError,
27+
ConflictError,
28+
ValidationError,
29+
RateLimitError,
30+
InternalError,
31+
ServiceUnavailableError,
32+
toApiError,
33+
} from './errors';

0 commit comments

Comments
 (0)