-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdiscovery.zod.ts
More file actions
278 lines (244 loc) · 12.3 KB
/
discovery.zod.ts
File metadata and controls
278 lines (244 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
import { z } from 'zod';
import { HttpMethod } from '../shared/http.zod';
/**
* Service Status Enum
* Describes the operational state of a service in the discovery response.
*
* - `available` – Fully operational: service is registered AND HTTP handler is verified.
* - `registered` – Route is declared in the dispatcher table but the HTTP handler has
* not been verified (may 501 at runtime).
* - `unavailable` – Service is not installed / not registered in the kernel.
* - `degraded` – Partially working (e.g., in-memory fallback, missing persistence).
* - `stub` – Placeholder handler that always returns 501 Not Implemented.
*/
export const ServiceStatus = z.enum([
'available',
'registered',
'unavailable',
'degraded',
'stub',
]).describe(
'available = fully operational, registered = route declared but handler unverified, '
+ 'unavailable = not installed, degraded = partial, stub = placeholder that returns 501'
);
export type ServiceStatus = z.infer<typeof ServiceStatus>;
/**
* Service Status in Discovery Response
* Reports per-service availability so clients can adapt their UI accordingly.
*/
export const ServiceInfoSchema = z.object({
/** Whether the service is enabled and available */
enabled: z.boolean(),
/** Current operational status */
status: ServiceStatus,
/**
* Whether the HTTP handler for this service is confirmed to be mounted.
*
* Semantics:
* - `undefined` (omitted) = handler readiness is unknown / not yet verified.
* - `true` = handler is registered in the adapter / dispatcher (safe to call).
* - `false` = route is declared but no handler exists or only a stub is present
* — requests are expected to receive 501 Not Implemented.
*
* Clients SHOULD check this flag before displaying or invoking a service endpoint and may
* distinguish between "unknown" (omitted) and "known missing" (`false`).
*/
handlerReady: z.boolean().optional().describe(
'Whether the HTTP handler is confirmed to be mounted. '
+ 'Omitted = readiness unknown/unverified; true = handler mounted; false = handler missing or stub (likely 501).'
),
/** Route path (only present if enabled) */
route: z.string().optional().describe('e.g. /api/v1/analytics'),
/** Implementation provider name */
provider: z.string().optional().describe('e.g. "objectql", "plugin-redis", "driver-memory"'),
/** Service version */
version: z.string().optional().describe('Semantic version of the service implementation (e.g. "3.0.6")'),
/** Human-readable reason if unavailable */
message: z.string().optional().describe('e.g. "Install plugin-workflow to enable"'),
/** Rate limit configuration for this service */
rateLimit: z.object({
requestsPerMinute: z.number().int().optional().describe('Maximum requests per minute'),
requestsPerHour: z.number().int().optional().describe('Maximum requests per hour'),
burstLimit: z.number().int().optional().describe('Maximum burst request count'),
retryAfterMs: z.number().int().optional().describe('Suggested retry-after delay in milliseconds when rate-limited'),
}).optional().describe('Rate limit and quota info for this service'),
});
/**
* API Routes Schema
* The "Map" for the frontend to know where to send requests.
* This decouples the frontend from hardcoded URL paths.
*/
export const ApiRoutesSchema = z.object({
/** Base URL for Object CRUD (Data Protocol) */
data: z.string().describe('e.g. /api/v1/data'),
/** Base URL for Schema Definitions (Metadata Protocol) */
metadata: z.string().describe('e.g. /api/v1/meta'),
/** Base URL for API Discovery endpoint */
discovery: z.string().optional().describe('e.g. /api/v1/discovery'),
/** Base URL for UI Configurations (Views, Menus) */
ui: z.string().optional().describe('e.g. /api/v1/ui'),
/** Base URL for Authentication (plugin-provided) */
auth: z.string().optional().describe('e.g. /api/v1/auth'),
/** Base URL for Automation (Flows/Scripts) */
automation: z.string().optional().describe('e.g. /api/v1/automation'),
/** Base URL for File/Storage operations */
storage: z.string().optional().describe('e.g. /api/v1/storage'),
/** Base URL for Analytics/BI operations */
analytics: z.string().optional().describe('e.g. /api/v1/analytics'),
/** GraphQL Endpoint (if enabled) */
graphql: z.string().optional().describe('e.g. /graphql'),
/** Base URL for Package Management */
packages: z.string().optional().describe('e.g. /api/v1/packages'),
/** Base URL for Workflow Engine */
workflow: z.string().optional().describe('e.g. /api/v1/workflow'),
/** Base URL for Realtime (WebSocket/SSE) */
realtime: z.string().optional().describe('e.g. /api/v1/realtime'),
/** Base URL for Notification Service */
notifications: z.string().optional().describe('e.g. /api/v1/notifications'),
/** Base URL for AI Engine (NLQ, Chat, Suggest) */
ai: z.string().optional().describe('e.g. /api/v1/ai'),
/** Base URL for Internationalization */
i18n: z.string().optional().describe('e.g. /api/v1/i18n'),
/** Base URL for Feed / Chatter API */
feed: z.string().optional().describe('e.g. /api/v1/feed'),
});
/**
* Discovery Response Schema
* The root object returned by the Metadata Discovery Endpoint.
*
* Design rationale:
* - `services` is the single source of truth for service availability.
* Each service entry includes `enabled`, `status`, `route`, and `provider`.
* - `routes` is a convenience shortcut: a flat map of service-name → route-path
* so that clients can resolve endpoints without iterating the services map.
* - `capabilities`/`features` was removed because it was fully derivable
* from `services[x].enabled`. Use `services` to determine feature availability.
*/
export const DiscoverySchema = z.object({
/** System Identity */
name: z.string(),
version: z.string(),
environment: z.enum(['production', 'sandbox', 'development']),
/** Dynamic Routing — convenience shortcut for client routing */
routes: ApiRoutesSchema,
/** Localization Info (helping frontend init i18n) */
locale: z.object({
default: z.string(),
supported: z.array(z.string()),
timezone: z.string(),
}),
/**
* Per-service status map.
* This is the **single source of truth** for service availability.
* Clients use this to determine which features are available,
* show/hide UI elements, and display appropriate messages.
*/
services: z.record(z.string(), ServiceInfoSchema).describe(
'Per-service availability map keyed by CoreServiceName'
),
/**
* Hierarchical capability descriptors.
* Declares platform features so clients can adapt UI without probing individual services.
* Each key is a capability domain (e.g., "comments", "automation", "search"),
* and its value describes what sub-features are available.
*/
capabilities: z.record(z.string(), z.object({
enabled: z.boolean().describe('Whether this capability is available'),
features: z.record(z.string(), z.boolean()).optional()
.describe('Sub-feature flags within this capability'),
description: z.string().optional()
.describe('Human-readable capability description'),
})).optional().describe('Hierarchical capability descriptors for frontend intelligent adaptation'),
/**
* Schema discovery URLs for cross-ecosystem interoperability.
*/
schemaDiscovery: z.object({
openapi: z.string().optional().describe('URL to OpenAPI (Swagger) specification (e.g., "/api/v1/openapi.json")'),
graphql: z.string().optional().describe('URL to GraphQL schema endpoint (e.g., "/graphql")'),
jsonSchema: z.string().optional().describe('URL to JSON Schema definitions'),
}).optional().describe('Schema discovery endpoints for API toolchain integration'),
/**
* Custom metadata key-value pairs for extensibility
*/
metadata: z.record(z.string(), z.unknown()).optional().describe('Custom metadata key-value pairs for extensibility'),
});
/**
* Well-Known Capabilities Schema
* Flat boolean flags for quick feature detection by clients (ObjectUI).
* Each flag indicates whether the backend supports a specific capability.
* Clients can use these to show/hide UI elements without probing individual endpoints.
*/
export const WellKnownCapabilitiesSchema = z.object({
/** Whether the backend supports Feed / Chatter API */
feed: z.boolean().describe('Whether the backend supports Feed / Chatter API'),
/** Whether the backend supports comments (a subset of Feed) */
comments: z.boolean().describe('Whether the backend supports comments (a subset of Feed)'),
/** Whether the backend supports Automation CRUD (flows, triggers) */
automation: z.boolean().describe('Whether the backend supports Automation CRUD (flows, triggers)'),
/** Whether the backend supports cron scheduling */
cron: z.boolean().describe('Whether the backend supports cron scheduling'),
/** Whether the backend supports full-text search */
search: z.boolean().describe('Whether the backend supports full-text search'),
/** Whether the backend supports async export */
export: z.boolean().describe('Whether the backend supports async export'),
/** Whether the backend supports chunked (multipart) uploads */
chunkedUpload: z.boolean().describe('Whether the backend supports chunked (multipart) uploads'),
}).describe('Well-known capability flags for frontend intelligent adaptation');
export type WellKnownCapabilities = z.infer<typeof WellKnownCapabilitiesSchema>;
export type DiscoveryResponse = z.infer<typeof DiscoverySchema>;
export type ApiRoutes = z.infer<typeof ApiRoutesSchema>;
export type ServiceInfo = z.infer<typeof ServiceInfoSchema>;
// ============================================================================
// Route Health Report
// ============================================================================
/**
* Single route health entry for the coverage report.
*/
export const RouteHealthEntrySchema = z.object({
/** Route path (e.g. /api/v1/analytics) */
route: z.string().describe('Route path pattern'),
/** HTTP method */
method: HttpMethod.describe('HTTP method (GET, POST, etc.)'),
/** Target service name */
service: z.string().describe('Target service name'),
/** Whether the route is declared in discovery */
declared: z.boolean().describe('Whether the route is declared in discovery/metadata'),
/** Whether the handler is actually registered in the adapter/dispatcher */
handlerRegistered: z.boolean().describe('Whether the HTTP handler is registered'),
/**
* Health check result:
* - `pass` – Handler exists and responds (2xx/4xx — i.e., not 404/501/503)
* - `fail` – Handler returned 501 or 503
* - `missing` – No handler registered (404)
* - `skip` – Health check was not performed
*/
healthStatus: z.enum(['pass', 'fail', 'missing', 'skip']).describe(
'pass = handler responds, fail = 501/503, missing = no handler (404), skip = not checked'
),
/** Optional diagnostic message */
message: z.string().optional().describe('Diagnostic message'),
});
export type RouteHealthEntry = z.infer<typeof RouteHealthEntrySchema>;
/**
* Route Health Report Schema
* Aggregated route coverage report produced at startup or on demand.
*
* This report enables automated detection of routes that are declared
* in discovery metadata but have no corresponding HTTP handler.
*/
export const RouteHealthReportSchema = z.object({
/** ISO 8601 timestamp of when the report was generated */
timestamp: z.string().describe('ISO 8601 timestamp of report generation'),
/** Adapter name that generated the report (e.g. "hono", "express", "nextjs") */
adapter: z.string().describe('Adapter or runtime that produced this report'),
/** Total routes declared in discovery / dispatcher table */
totalDeclared: z.number().int().describe('Total routes declared in discovery'),
/** Routes with a confirmed handler registration */
totalRegistered: z.number().int().describe('Routes with confirmed handler'),
/** Routes missing a handler */
totalMissing: z.number().int().describe('Routes missing a handler'),
/** Per-route health entries */
routes: z.array(RouteHealthEntrySchema).describe('Per-route health entries'),
});
export type RouteHealthReport = z.infer<typeof RouteHealthReportSchema>;