|
| 1 | +# API Registry Implementation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The API Registry is a centralized service in the ObjectStack kernel that manages API endpoint registration, discovery, and conflict resolution across different protocols and plugins. |
| 6 | + |
| 7 | +## Features |
| 8 | + |
| 9 | +✅ **Multi-Protocol Support** - REST, GraphQL, OData, WebSocket, Plugin APIs, and more |
| 10 | +✅ **Route Conflict Detection** - Configurable strategies (error, priority, first-wins, last-wins) |
| 11 | +✅ **RBAC Integration** - Endpoints can specify required permissions |
| 12 | +✅ **Dynamic Schema Linking** - Reference ObjectQL objects for auto-updating schemas |
| 13 | +✅ **Protocol Extensions** - Support for gRPC, tRPC, and custom protocols |
| 14 | +✅ **API Discovery** - Filter and search APIs by type, status, tags, and more |
| 15 | + |
| 16 | +## Architecture |
| 17 | + |
| 18 | +The API Registry follows the ObjectStack microkernel pattern: |
| 19 | + |
| 20 | +``` |
| 21 | +┌─────────────────────────────────────────────────────┐ |
| 22 | +│ ObjectKernel (Core) │ |
| 23 | +│ ┌───────────────────────────────────────────────┐ │ |
| 24 | +│ │ Service Registry (DI Container) │ │ |
| 25 | +│ │ ┌─────────────────────────────────────────┐ │ │ |
| 26 | +│ │ │ API Registry Service │ │ │ |
| 27 | +│ │ │ • registerApi() │ │ │ |
| 28 | +│ │ │ • unregisterApi() │ │ │ |
| 29 | +│ │ │ • findApis() │ │ │ |
| 30 | +│ │ │ • getRegistry() │ │ │ |
| 31 | +│ │ └─────────────────────────────────────────┘ │ │ |
| 32 | +│ └───────────────────────────────────────────────┘ │ |
| 33 | +└─────────────────────────────────────────────────────┘ |
| 34 | + │ |
| 35 | + ┌─────────┴─────────┬──────────┬──────────┐ |
| 36 | + │ │ │ │ |
| 37 | +┌───▼────┐ ┌───────▼──┐ ┌──▼───┐ ┌───▼────┐ |
| 38 | +│ REST │ │ GraphQL │ │WebSkt│ │ Plugin │ |
| 39 | +│ Plugin │ │ Plugin │ │Plugin│ │ APIs │ |
| 40 | +└────────┘ └──────────┘ └──────┘ └────────┘ |
| 41 | +``` |
| 42 | + |
| 43 | +## Usage |
| 44 | + |
| 45 | +### 1. Register the API Registry Plugin |
| 46 | + |
| 47 | +```typescript |
| 48 | +import { ObjectKernel, createApiRegistryPlugin } from '@objectstack/core'; |
| 49 | + |
| 50 | +const kernel = new ObjectKernel(); |
| 51 | + |
| 52 | +// Register with default settings (error on conflicts) |
| 53 | +kernel.use(createApiRegistryPlugin()); |
| 54 | + |
| 55 | +// Or with custom configuration |
| 56 | +kernel.use( |
| 57 | + createApiRegistryPlugin({ |
| 58 | + conflictResolution: 'priority', // priority, first-wins, last-wins |
| 59 | + version: '1.0.0', |
| 60 | + }) |
| 61 | +); |
| 62 | + |
| 63 | +await kernel.bootstrap(); |
| 64 | +``` |
| 65 | + |
| 66 | +### 2. Register APIs in Plugins |
| 67 | + |
| 68 | +```typescript |
| 69 | +import type { Plugin } from '@objectstack/core'; |
| 70 | +import type { ApiRegistry } from '@objectstack/core'; |
| 71 | +import type { ApiRegistryEntry } from '@objectstack/spec/api'; |
| 72 | + |
| 73 | +const myPlugin: Plugin = { |
| 74 | + name: 'my-plugin', |
| 75 | + version: '1.0.0', |
| 76 | + |
| 77 | + init: async (ctx) => { |
| 78 | + // Get the API Registry service |
| 79 | + const registry = ctx.getService<ApiRegistry>('api-registry'); |
| 80 | + |
| 81 | + // Register your API |
| 82 | + const api: ApiRegistryEntry = { |
| 83 | + id: 'customer_api', |
| 84 | + name: 'Customer API', |
| 85 | + type: 'rest', |
| 86 | + version: 'v1', |
| 87 | + basePath: '/api/v1/customers', |
| 88 | + endpoints: [ |
| 89 | + { |
| 90 | + id: 'get_customer', |
| 91 | + method: 'GET', |
| 92 | + path: '/api/v1/customers/:id', |
| 93 | + summary: 'Get customer by ID', |
| 94 | + requiredPermissions: ['customer.read'], // RBAC |
| 95 | + parameters: [ |
| 96 | + { |
| 97 | + name: 'id', |
| 98 | + in: 'path', |
| 99 | + required: true, |
| 100 | + schema: { type: 'string', format: 'uuid' }, |
| 101 | + }, |
| 102 | + ], |
| 103 | + responses: [ |
| 104 | + { |
| 105 | + statusCode: 200, |
| 106 | + description: 'Customer found', |
| 107 | + schema: { |
| 108 | + $ref: { |
| 109 | + objectId: 'customer', // Dynamic ObjectQL reference |
| 110 | + excludeFields: ['password_hash'], |
| 111 | + }, |
| 112 | + }, |
| 113 | + }, |
| 114 | + ], |
| 115 | + }, |
| 116 | + ], |
| 117 | + metadata: { |
| 118 | + status: 'active', |
| 119 | + tags: ['customer', 'crm'], |
| 120 | + }, |
| 121 | + }; |
| 122 | + |
| 123 | + registry.registerApi(api); |
| 124 | + }, |
| 125 | +}; |
| 126 | +``` |
| 127 | + |
| 128 | +### 3. Discover APIs |
| 129 | + |
| 130 | +```typescript |
| 131 | +const registry = kernel.getService<ApiRegistry>('api-registry'); |
| 132 | + |
| 133 | +// Get all APIs |
| 134 | +const allApis = registry.getAllApis(); |
| 135 | + |
| 136 | +// Find REST APIs |
| 137 | +const restApis = registry.findApis({ type: 'rest' }); |
| 138 | + |
| 139 | +// Find active APIs with specific tags |
| 140 | +const crmApis = registry.findApis({ |
| 141 | + status: 'active', |
| 142 | + tags: ['crm'], |
| 143 | +}); |
| 144 | + |
| 145 | +// Search by name |
| 146 | +const searchResults = registry.findApis({ |
| 147 | + search: 'customer', |
| 148 | +}); |
| 149 | + |
| 150 | +// Get endpoint by route |
| 151 | +const endpoint = registry.findEndpointByRoute('GET', '/api/v1/customers/:id'); |
| 152 | +console.log(endpoint?.api.name); // "Customer API" |
| 153 | +console.log(endpoint?.endpoint.summary); // "Get customer by ID" |
| 154 | +``` |
| 155 | + |
| 156 | +### 4. Get Registry Snapshot |
| 157 | + |
| 158 | +```typescript |
| 159 | +const registry = kernel.getService<ApiRegistry>('api-registry'); |
| 160 | +const snapshot = registry.getRegistry(); |
| 161 | + |
| 162 | +console.log(`Total APIs: ${snapshot.totalApis}`); |
| 163 | +console.log(`Total Endpoints: ${snapshot.totalEndpoints}`); |
| 164 | +console.log(`Conflict Resolution: ${snapshot.conflictResolution}`); |
| 165 | + |
| 166 | +// APIs grouped by type |
| 167 | +snapshot.byType?.rest.forEach((api) => { |
| 168 | + console.log(`REST API: ${api.name}`); |
| 169 | +}); |
| 170 | + |
| 171 | +// APIs grouped by status |
| 172 | +snapshot.byStatus?.active.forEach((api) => { |
| 173 | + console.log(`Active API: ${api.name}`); |
| 174 | +}); |
| 175 | +``` |
| 176 | + |
| 177 | +## Conflict Resolution Strategies |
| 178 | + |
| 179 | +### 1. Error (Default) |
| 180 | + |
| 181 | +Throws an error when a route conflict is detected. |
| 182 | + |
| 183 | +```typescript |
| 184 | +kernel.use(createApiRegistryPlugin({ conflictResolution: 'error' })); |
| 185 | +``` |
| 186 | + |
| 187 | +**Best for:** Production environments where conflicts should be caught early. |
| 188 | + |
| 189 | +### 2. Priority |
| 190 | + |
| 191 | +Uses the `priority` field on endpoints to resolve conflicts. Higher priority wins. |
| 192 | + |
| 193 | +```typescript |
| 194 | +kernel.use(createApiRegistryPlugin({ conflictResolution: 'priority' })); |
| 195 | + |
| 196 | +// In your plugin |
| 197 | +registry.registerApi({ |
| 198 | + endpoints: [ |
| 199 | + { |
| 200 | + path: '/api/data/:object', |
| 201 | + priority: 900, // Core API (high priority) |
| 202 | + }, |
| 203 | + ], |
| 204 | +}); |
| 205 | +``` |
| 206 | + |
| 207 | +**Priority Ranges:** |
| 208 | +- **900-1000**: Core system endpoints |
| 209 | +- **500-900**: Custom/override endpoints |
| 210 | +- **100-500**: Plugin endpoints |
| 211 | +- **0-100**: Fallback routes |
| 212 | + |
| 213 | +### 3. First-Wins |
| 214 | + |
| 215 | +First registered endpoint wins. Subsequent registrations are ignored. |
| 216 | + |
| 217 | +```typescript |
| 218 | +kernel.use(createApiRegistryPlugin({ conflictResolution: 'first-wins' })); |
| 219 | +``` |
| 220 | + |
| 221 | +**Best for:** Stable, predictable routing where load order matters. |
| 222 | + |
| 223 | +### 4. Last-Wins |
| 224 | + |
| 225 | +Last registered endpoint wins. Previous registrations are overwritten. |
| 226 | + |
| 227 | +```typescript |
| 228 | +kernel.use(createApiRegistryPlugin({ conflictResolution: 'last-wins' })); |
| 229 | +``` |
| 230 | + |
| 231 | +**Best for:** Development/testing where you want to override defaults. |
| 232 | + |
| 233 | +## RBAC Integration |
| 234 | + |
| 235 | +Endpoints can specify required permissions that are automatically validated at the gateway level: |
| 236 | + |
| 237 | +```typescript |
| 238 | +{ |
| 239 | + id: 'delete_customer', |
| 240 | + method: 'DELETE', |
| 241 | + path: '/api/v1/customers/:id', |
| 242 | + requiredPermissions: [ |
| 243 | + 'customer.delete', |
| 244 | + 'api_enabled', |
| 245 | + ], |
| 246 | + responses: [], |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +**Permission Format:** |
| 251 | +- **Object Permissions:** `<object>.<operation>` (e.g., `customer.read`, `order.delete`) |
| 252 | +- **System Permissions:** `<permission_name>` (e.g., `manage_users`, `api_enabled`) |
| 253 | + |
| 254 | +## Dynamic Schema Linking |
| 255 | + |
| 256 | +Reference ObjectQL objects instead of static schemas: |
| 257 | + |
| 258 | +```typescript |
| 259 | +{ |
| 260 | + statusCode: 200, |
| 261 | + description: 'Customer retrieved', |
| 262 | + schema: { |
| 263 | + $ref: { |
| 264 | + objectId: 'customer', // ObjectQL object name |
| 265 | + excludeFields: ['password_hash'], // Exclude sensitive fields |
| 266 | + includeFields: ['id', 'name'], // Or whitelist specific fields |
| 267 | + includeRelated: ['account'], // Include related objects |
| 268 | + }, |
| 269 | + }, |
| 270 | +} |
| 271 | +``` |
| 272 | + |
| 273 | +**Benefits:** |
| 274 | +- API documentation auto-updates when object schemas change |
| 275 | +- No schema duplication between API and data model |
| 276 | +- Consistent type definitions across API and database |
| 277 | + |
| 278 | +## Protocol-Specific Configuration |
| 279 | + |
| 280 | +Support custom protocols with `protocolConfig`: |
| 281 | + |
| 282 | +### WebSocket |
| 283 | + |
| 284 | +```typescript |
| 285 | +{ |
| 286 | + id: 'customer_updates', |
| 287 | + path: '/ws/customers', |
| 288 | + protocolConfig: { |
| 289 | + subProtocol: 'websocket', |
| 290 | + eventName: 'customer.updated', |
| 291 | + direction: 'server-to-client', |
| 292 | + }, |
| 293 | +} |
| 294 | +``` |
| 295 | + |
| 296 | +### gRPC |
| 297 | + |
| 298 | +```typescript |
| 299 | +{ |
| 300 | + id: 'grpc_method', |
| 301 | + path: '/grpc/CustomerService/GetCustomer', |
| 302 | + protocolConfig: { |
| 303 | + subProtocol: 'grpc', |
| 304 | + serviceName: 'CustomerService', |
| 305 | + methodName: 'GetCustomer', |
| 306 | + streaming: false, |
| 307 | + }, |
| 308 | +} |
| 309 | +``` |
| 310 | + |
| 311 | +### tRPC |
| 312 | + |
| 313 | +```typescript |
| 314 | +{ |
| 315 | + id: 'trpc_query', |
| 316 | + path: '/trpc/customer.get', |
| 317 | + protocolConfig: { |
| 318 | + subProtocol: 'trpc', |
| 319 | + procedureType: 'query', |
| 320 | + router: 'customer', |
| 321 | + }, |
| 322 | +} |
| 323 | +``` |
| 324 | + |
| 325 | +## API Registry Methods |
| 326 | + |
| 327 | +### Registration |
| 328 | + |
| 329 | +- `registerApi(api: ApiRegistryEntry): void` - Register an API |
| 330 | +- `unregisterApi(apiId: string): void` - Unregister an API |
| 331 | + |
| 332 | +### Discovery |
| 333 | + |
| 334 | +- `getApi(apiId: string): ApiRegistryEntry | undefined` - Get API by ID |
| 335 | +- `getAllApis(): ApiRegistryEntry[]` - Get all registered APIs |
| 336 | +- `findApis(query: ApiDiscoveryQuery): ApiDiscoveryResponse` - Search/filter APIs |
| 337 | +- `getEndpoint(apiId: string, endpointId: string): ApiEndpointRegistration | undefined` - Get specific endpoint |
| 338 | +- `findEndpointByRoute(method: string, path: string): { api, endpoint } | undefined` - Find endpoint by route |
| 339 | + |
| 340 | +### Registry Info |
| 341 | + |
| 342 | +- `getRegistry(): ApiRegistry` - Get complete registry snapshot |
| 343 | +- `getStats(): RegistryStats` - Get registry statistics |
| 344 | +- `clear(): void` - Clear all registered APIs (for testing) |
| 345 | + |
| 346 | +## Examples |
| 347 | + |
| 348 | +See [api-registry-example.ts](./examples/api-registry-example.ts) for comprehensive examples: |
| 349 | + |
| 350 | +1. **Basic API Registration** - Simple REST API with CRUD endpoints |
| 351 | +2. **Multi-Plugin Discovery** - Multiple plugins registering different API types |
| 352 | +3. **Route Conflict Resolution** - Priority-based conflict handling |
| 353 | +4. **Custom Protocol Support** - WebSocket API with protocol config |
| 354 | +5. **Dynamic Schema Linking** - ObjectQL reference in API responses |
| 355 | + |
| 356 | +## Testing |
| 357 | + |
| 358 | +Run the API Registry tests: |
| 359 | + |
| 360 | +```bash |
| 361 | +pnpm --filter @objectstack/core test api-registry.test.ts |
| 362 | +pnpm --filter @objectstack/core test api-registry-plugin.test.ts |
| 363 | +``` |
| 364 | + |
| 365 | +**Test Coverage:** |
| 366 | +- ✅ 32 tests for ApiRegistry service |
| 367 | +- ✅ 9 tests for API Registry plugin |
| 368 | +- ✅ All conflict resolution strategies |
| 369 | +- ✅ Multi-protocol support |
| 370 | +- ✅ API discovery and filtering |
| 371 | +- ✅ Integration with kernel lifecycle |
| 372 | + |
| 373 | +## Next Steps |
| 374 | + |
| 375 | +Based on [API_REGISTRY_ENHANCEMENTS.md](../../API_REGISTRY_ENHANCEMENTS.md), recommended next implementations: |
| 376 | + |
| 377 | +1. **API Explorer Plugin** - UI to visualize the registry |
| 378 | +2. **Gateway Integration** - Implement permission checking in API gateway |
| 379 | +3. **Schema Resolution** - Build engine to resolve ObjectQL references to JSON schemas |
| 380 | +4. **Conflict Detection UI** - Visualization of route conflicts and priorities |
| 381 | +5. **Plugin Examples** - Reference implementations for gRPC and tRPC plugins |
| 382 | + |
| 383 | +## Related Documentation |
| 384 | + |
| 385 | +- [API Registry Schema](../spec/src/api/registry.zod.ts) - Zod schema definitions |
| 386 | +- [API Registry Tests](./src/api-registry.test.ts) - Comprehensive test suite |
| 387 | +- [Plugin System](./README.md) - ObjectStack plugin architecture |
| 388 | +- [Microkernel Design](../../ARCHITECTURE.md) - Overall architecture |
| 389 | + |
| 390 | +## License |
| 391 | + |
| 392 | +MIT |
0 commit comments