@capixjs/core 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/capability.d.ts +165 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +268 -0
- package/dist/capability.js.map +1 -0
- package/dist/context.d.ts +35 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +28 -0
- package/dist/context.js.map +1 -0
- package/dist/enhancers.d.ts +92 -0
- package/dist/enhancers.d.ts.map +1 -0
- package/dist/enhancers.js +251 -0
- package/dist/enhancers.js.map +1 -0
- package/dist/errors.d.ts +53 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +74 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +56 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +51 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +39 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +210 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/guards.d.ts +78 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +56 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +28 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +43 -0
- package/dist/plugin.js.map +1 -0
- package/dist/server.d.ts +70 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +97 -0
- package/dist/server.js.map +1 -0
- package/dist/type-tests.d.ts +12 -0
- package/dist/type-tests.d.ts.map +1 -0
- package/dist/type-tests.js +175 -0
- package/dist/type-tests.js.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Capix Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# capix
|
|
2
|
+
|
|
3
|
+
Core primitives for the Capix framework. Defines capabilities, context, guards, errors, enhancers, plugins, and the server.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @capixjs/core zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick example
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { capability, defineContext, defineGuard, defineError, createServer } from '@capixjs/core';
|
|
16
|
+
import { restTransport } from '@capixjs/transport-rest';
|
|
17
|
+
|
|
18
|
+
// 1. Define what your server knows about each request
|
|
19
|
+
const buildContext = defineContext(async (req) => ({
|
|
20
|
+
requestId: crypto.randomUUID(),
|
|
21
|
+
user: req.headers.authorization === 'Bearer admin'
|
|
22
|
+
? { id: '1', role: 'admin' as const }
|
|
23
|
+
: null,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
// 2. Define guards — preconditions that run before a capability
|
|
27
|
+
const Errors = { Unauthorized: defineError(401, 'Unauthorized') };
|
|
28
|
+
const mustBeUser = defineGuard((ctx) => {
|
|
29
|
+
if (!ctx.user) throw Errors.Unauthorized();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// 3. Define capabilities — typed pure functions
|
|
33
|
+
const cap = capability.withContext<Awaited<ReturnType<typeof buildContext>>>();
|
|
34
|
+
const getUser = cap(
|
|
35
|
+
z.object({ id: z.string() }),
|
|
36
|
+
async ({ id }) => ({ id, name: 'Alice' }),
|
|
37
|
+
'query',
|
|
38
|
+
).guard(mustBeUser);
|
|
39
|
+
|
|
40
|
+
// 4. Create the server
|
|
41
|
+
createServer({
|
|
42
|
+
context: buildContext,
|
|
43
|
+
capabilities: { users: { getUser } },
|
|
44
|
+
transports: [restTransport({ port: 3000 })],
|
|
45
|
+
}).start();
|
|
46
|
+
// → GET /users/:id
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Capability
|
|
50
|
+
|
|
51
|
+
A capability wraps a resolver function with optional input/output schemas:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// No schema — no input validation
|
|
55
|
+
const ping = capability(() => 'pong');
|
|
56
|
+
|
|
57
|
+
// With input schema — validates and infers types
|
|
58
|
+
const greet = capability(
|
|
59
|
+
z.object({ name: z.string() }),
|
|
60
|
+
({ name }) => `Hello, ${name}`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// With output schema — validates resolver return value in development
|
|
64
|
+
const getMetrics = capability(
|
|
65
|
+
z.object({ window: z.enum(['1h', '24h']) }),
|
|
66
|
+
fetchMetrics,
|
|
67
|
+
).output(MetricsSchema);
|
|
68
|
+
|
|
69
|
+
// With explicit intent (overrides name-based inference)
|
|
70
|
+
const searchUsers = capability(
|
|
71
|
+
z.object({ q: z.string() }),
|
|
72
|
+
({ q }) => db.users.search(q),
|
|
73
|
+
'query',
|
|
74
|
+
);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Capabilities are **immutable**. `.guard()`, `.enhance()`, and `.output()` each return a new instance:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const adminOnly = greet.guard(mustBeAdmin);
|
|
81
|
+
const cached = getMetrics.enhance(withCache(60));
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Context-typed factory
|
|
85
|
+
|
|
86
|
+
Use `capability.withContext<TContext>()` to bind the factory to your application context type. Define it once, import everywhere:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// src/capabilities.ts
|
|
90
|
+
import { capability } from '@capixjs/core';
|
|
91
|
+
import type { AppContext } from './context.js';
|
|
92
|
+
|
|
93
|
+
export const cap = capability.withContext<AppContext>();
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For capabilities that require a logged-in user, define a second factory with a narrowed context:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
type AuthContext = AppContext & { user: NonNullable<AppContext['user']> };
|
|
100
|
+
|
|
101
|
+
export const cap = capability.withContext<AppContext>(); // public endpoints
|
|
102
|
+
export const authCap = capability.withContext<AuthContext>(); // authenticated endpoints
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Without `withContext`, `ctx` is typed as `BaseContext` (only `requestId`).
|
|
106
|
+
|
|
107
|
+
### Internal composition with `.resolve()`
|
|
108
|
+
|
|
109
|
+
Call one capability from inside another. Guards re-run — this is intentional and safe:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
export const getPost = cap(z.object({ id: z.string() }), async ({ id }) => {
|
|
113
|
+
const post = await db.posts.find(id);
|
|
114
|
+
if (!post) throw Errors.NotFound();
|
|
115
|
+
return post;
|
|
116
|
+
}, 'query').guard(mustBeUser);
|
|
117
|
+
|
|
118
|
+
export const updatePost = cap(z.object({ id: z.string(), title: z.string() }), async ({ id, title }, ctx) => {
|
|
119
|
+
const post = await getPost.resolve({ id }, ctx); // guards re-run
|
|
120
|
+
return db.posts.update(id, { title });
|
|
121
|
+
}, 'update').guard(mustBeUser);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Guards
|
|
125
|
+
|
|
126
|
+
Guards run before the resolver and throw to reject the request:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { defineGuard, defineError } from '@capixjs/core';
|
|
130
|
+
|
|
131
|
+
const Errors = { Forbidden: defineError(403, 'Forbidden') };
|
|
132
|
+
|
|
133
|
+
const mustBeAdmin = defineGuard((ctx) => {
|
|
134
|
+
if (ctx.user?.role !== 'admin') throw Errors.Forbidden();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Multiple guards run in order; first failure stops execution
|
|
138
|
+
const adminCap = capability(schema, handler)
|
|
139
|
+
.guard(mustBeLoggedIn)
|
|
140
|
+
.guard(mustBeAdmin);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Input guards** run after input validation and receive both `(input, ctx)`:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { defineInputGuard } from '@capixjs/core';
|
|
147
|
+
|
|
148
|
+
const mustOwnResource = defineInputGuard((input: { id: string }, ctx) => {
|
|
149
|
+
if (input.id !== ctx.user?.id) throw Errors.Forbidden();
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Errors
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { defineError, defaultErrors } from '@capixjs/core';
|
|
157
|
+
|
|
158
|
+
// Define application-specific errors
|
|
159
|
+
const Errors = {
|
|
160
|
+
NotFound: defineError(404, 'Not found'),
|
|
161
|
+
OutOfStock: defineError(409, 'Out of stock'),
|
|
162
|
+
// Explicit code — predictable, easy to test:
|
|
163
|
+
NotPurchased: defineError(403, 'You can only review products you have purchased', 'NotPurchased'),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// Use built-in errors
|
|
167
|
+
throw defaultErrors.Unauthorized();
|
|
168
|
+
throw defaultErrors.NotFound({ detail: 'User not found' });
|
|
169
|
+
|
|
170
|
+
// Use custom errors
|
|
171
|
+
throw Errors.OutOfStock({ productId: '123' });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The error code in responses is derived from the message (`'Not found'` → `'NotFound'`) unless you pass a third argument. Errors are serialized as:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{ "error": "NotFound", "message": "Not found" }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
with the given HTTP status code.
|
|
181
|
+
|
|
182
|
+
## Enhancers
|
|
183
|
+
|
|
184
|
+
Enhancers wrap the resolver for cross-cutting concerns:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { withCache, withRateLimit, withCircuitBreaker, withTimeout, withRetry, withMetrics } from '@capixjs/core';
|
|
188
|
+
|
|
189
|
+
const robustCap = capability(schema, handler)
|
|
190
|
+
.enhance(withCache(30))
|
|
191
|
+
.enhance(withRateLimit({ max: 100, windowMs: 60_000 }))
|
|
192
|
+
.enhance(withCircuitBreaker({ failureThreshold: 5, successThreshold: 2, timeoutMs: 30_000 }))
|
|
193
|
+
.enhance(withTimeout(5000))
|
|
194
|
+
.enhance(withRetry(3, 200));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
| Enhancer | Options | Description |
|
|
198
|
+
|---|---|---|
|
|
199
|
+
| `withCache(ttlSeconds)` | `number` | In-memory cache keyed by capability name + serialized input |
|
|
200
|
+
| `withRateLimit(opts)` | `{ max, windowMs }` | Sliding window rate limiter |
|
|
201
|
+
| `withCircuitBreaker(opts)` | `{ failureThreshold, successThreshold, timeoutMs }` | Opens after N failures; resets after timeout |
|
|
202
|
+
| `withTimeout(ms)` | `number` | Rejects after N milliseconds with `504 Timeout` |
|
|
203
|
+
| `withRetry(n, delayMs?)` | `number, number` | Retries on non-FrameworkError failures with backoff |
|
|
204
|
+
| `withRollback` | — | Enables `ctx.onRollback(fn)` for compensating failed multi-step mutations |
|
|
205
|
+
| `withMetrics(collector)` | `MetricsCollector` | Emits duration and success/error metrics |
|
|
206
|
+
| `withLogging` | — | Logs capability name, duration, and outcome |
|
|
207
|
+
|
|
208
|
+
## Context
|
|
209
|
+
|
|
210
|
+
Context is built once per request and passed to every guard and resolver:
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
import { defineContext } from '@capixjs/core';
|
|
214
|
+
|
|
215
|
+
const buildContext = defineContext(async (req) => ({
|
|
216
|
+
requestId: crypto.randomUUID(),
|
|
217
|
+
user: await verifyToken(req.headers.authorization),
|
|
218
|
+
db,
|
|
219
|
+
}));
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The `req` argument is `{ headers: Record<string, string | string[] | undefined> }`. Use `getHeader(req, 'authorization')` for safe string access.
|
|
223
|
+
|
|
224
|
+
## Plugins
|
|
225
|
+
|
|
226
|
+
Plugins encapsulate capabilities and context extensions:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { definePlugin } from '@capixjs/core';
|
|
230
|
+
|
|
231
|
+
const tenantPlugin = definePlugin({
|
|
232
|
+
capabilities: { users: { getUser, createUser } },
|
|
233
|
+
context: (base) => ({ ...base, tenantId: 'default' }),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
createServer({
|
|
237
|
+
plugins: [tenantPlugin, loggingPlugin],
|
|
238
|
+
transports: [restTransport({ port: 3000 })],
|
|
239
|
+
}).start();
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Event bus
|
|
243
|
+
|
|
244
|
+
Typed pub/sub for broadcasting events from REST capabilities to WebSocket clients:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { createEventBus } from '@capixjs/core';
|
|
248
|
+
|
|
249
|
+
type AppEvents = {
|
|
250
|
+
'order:paid': { orderId: string; amount: number };
|
|
251
|
+
'task:updated': { id: string; status: string };
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export const eventBus = createEventBus<AppEvents>();
|
|
255
|
+
|
|
256
|
+
// Publish from any capability or anywhere in your app
|
|
257
|
+
eventBus.publish('order:paid', { orderId: '123', amount: 99 });
|
|
258
|
+
|
|
259
|
+
// Subscribe server-side
|
|
260
|
+
const unsub = eventBus.subscribe('order:paid', (data) => {
|
|
261
|
+
console.log('Order paid:', data.orderId);
|
|
262
|
+
});
|
|
263
|
+
unsub(); // unsubscribe
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Pass `eventBus` to `wsTransport({ eventBus })` to forward published events to subscribed WS clients.
|
|
267
|
+
|
|
268
|
+
## Exports
|
|
269
|
+
|
|
270
|
+
### Factories
|
|
271
|
+
|
|
272
|
+
| Export | Description |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `capability()` | Create a capability |
|
|
275
|
+
| `capability.withContext<T>()` | Create a scoped factory with a pre-bound context type |
|
|
276
|
+
| `defineContext(fn)` | Define a context builder |
|
|
277
|
+
| `defineGuard(fn)` | Define a guard |
|
|
278
|
+
| `defineGuardFor<T>()` | Define a narrowing guard (asserts ctx is T) |
|
|
279
|
+
| `defineInputGuard(fn)` | Define an input guard (runs after validation, receives input + ctx) |
|
|
280
|
+
| `defineError(status, message, code?)` | Define a typed error factory |
|
|
281
|
+
| `defineEnhancer(fn)` | Define a capability enhancer |
|
|
282
|
+
| `definePlugin(plugin)` | Define a plugin |
|
|
283
|
+
| `defineConfig(config)` | Define server config with type inference |
|
|
284
|
+
| `createServer(config)` | Create a server |
|
|
285
|
+
| `createEventBus<TEvents>()` | Create a typed event bus |
|
|
286
|
+
|
|
287
|
+
### Built-in enhancers
|
|
288
|
+
|
|
289
|
+
| Export | Description |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `withCache(ttlSeconds)` | In-memory output cache |
|
|
292
|
+
| `withRateLimit(options)` | Sliding window rate limiter |
|
|
293
|
+
| `withCircuitBreaker(options)` | Circuit breaker (closed / open / half-open) |
|
|
294
|
+
| `withTimeout(ms)` | Cancel after N milliseconds |
|
|
295
|
+
| `withRetry(maxAttempts, delayMs?)` | Retry on non-framework failures with backoff |
|
|
296
|
+
| `withRollback` | Compensation registry for multi-step mutations |
|
|
297
|
+
| `withMetrics(collector)` | Record timing and outcome metrics |
|
|
298
|
+
| `withLogging` | Log capability name, duration, and outcome |
|
|
299
|
+
| `consoleMetricsCollector` | Default `MetricsCollector` that logs to console |
|
|
300
|
+
|
|
301
|
+
### Utilities
|
|
302
|
+
|
|
303
|
+
| Export | Description |
|
|
304
|
+
|---|---|
|
|
305
|
+
| `isCapability(v)` | Type guard for capabilities |
|
|
306
|
+
| `isFrameworkError(v)` | Type guard for FrameworkErrors |
|
|
307
|
+
| `inferIntent(key)` | Infer capability intent from a key name |
|
|
308
|
+
| `compileRegistry(capabilities)` | Compile a group tree into a flat registry |
|
|
309
|
+
| `getHeader(req, name)` | Safe header access from `RawRequest` |
|
|
310
|
+
| `defaultErrors` | Built-in error factories: `BadRequest`, `Unauthorized`, `Forbidden`, `NotFound`, `Conflict`, `TooManyRequests`, `Internal`, `Timeout` |
|
|
311
|
+
| `runGuards` / `runInputGuards` | Run guard arrays (used by custom transports) |
|
|
312
|
+
| `createExecutionEngine(options)` | Create a capability execution engine |
|
|
313
|
+
| `mergePlugins(plugins)` | Merge a plugin array into a single plugin |
|
|
314
|
+
|
|
315
|
+
### Types
|
|
316
|
+
|
|
317
|
+
`Capability<TInput, TOutput, TContext>`, `AnyCapability`, `BaseContext`, `ContextBuilder`,
|
|
318
|
+
`RawRequest`, `Guard`, `NarrowingGuard`, `InputGuard`, `AnyGuard`, `NarrowContext`,
|
|
319
|
+
`Enhancer`, `Resolver`, `Plugin`, `MergedPlugins`, `Intent`, `InferInput<Cap>`,
|
|
320
|
+
`InferOutput<Cap>`, `InferContext<Cap>`, `GroupTree`, `CapabilityRegistry`,
|
|
321
|
+
`ScopedCapabilityFactory`, `FrameworkError`, `ErrorFactory`, `WithRollback<T>`,
|
|
322
|
+
`MetricsCollector`, `CircuitBreakerOptions`, `RateLimitOptions`, `Transport`,
|
|
323
|
+
`TransportWithCapabilities`, `MountOptions`, `ServerConfig`, `Server`,
|
|
324
|
+
`EventBus<TEvents>`, `EventMap`, `SubscribeOptions`, `InvokeFn`, `CapabilityRequest`,
|
|
325
|
+
`CapabilityResponse`
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capability.ts — the core capability primitive
|
|
3
|
+
* Depends on: errors.ts, context.ts, guards.ts, zod
|
|
4
|
+
*/
|
|
5
|
+
import type { ZodSchema, ZodTypeAny } from 'zod';
|
|
6
|
+
import type { BaseContext } from './context.js';
|
|
7
|
+
import type { AnyGuard, AnyInputGuard, InputGuard, NarrowContext } from './guards.js';
|
|
8
|
+
declare const CAPABILITY_BRAND: unique symbol;
|
|
9
|
+
export type Intent = 'query' | 'mutation' | 'update' | 'replace' | 'delete';
|
|
10
|
+
/** Infers capability intent from its key name in the parent group. */
|
|
11
|
+
export declare function inferIntent(key: string): Intent;
|
|
12
|
+
export type Enhancer = <TInput, TOutput, TContext extends BaseContext>(cap: Capability<TInput, TOutput, TContext>) => Capability<TInput, TOutput, TContext>;
|
|
13
|
+
export type Resolver<TInput, TOutput, TContext extends BaseContext> = (input: TInput, ctx: TContext) => TOutput | Promise<TOutput>;
|
|
14
|
+
export type Capability<TInput, TOutput, TContext extends BaseContext> = {
|
|
15
|
+
readonly _capix: true;
|
|
16
|
+
readonly [CAPABILITY_BRAND]: true;
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly _input: TInput;
|
|
19
|
+
readonly _output: TOutput;
|
|
20
|
+
readonly _context: TContext;
|
|
21
|
+
readonly inputSchema: ZodTypeAny | null;
|
|
22
|
+
readonly outputSchema: ZodTypeAny | null;
|
|
23
|
+
readonly guards: ReadonlyArray<AnyGuard>;
|
|
24
|
+
readonly inputGuards: ReadonlyArray<AnyInputGuard>;
|
|
25
|
+
readonly intent: Intent;
|
|
26
|
+
/** True when intent was explicitly passed to capability(); false when defaulted. */
|
|
27
|
+
readonly _intentExplicit: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Set by compileRegistry when inputSchema is a z.object({}) with no keys.
|
|
30
|
+
* The execution engine skips input validation entirely — there is nothing to validate.
|
|
31
|
+
*/
|
|
32
|
+
readonly _skipValidation: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Invoke this capability's guards and resolver directly.
|
|
35
|
+
*
|
|
36
|
+
* Guards always re-run — this is safe to call from any context.
|
|
37
|
+
* If the context does not satisfy the guards, they throw as normal.
|
|
38
|
+
*
|
|
39
|
+
* TypeScript does not verify at the call site that the provided context
|
|
40
|
+
* satisfies this capability's guard requirements — guards enforce this
|
|
41
|
+
* at runtime. This enables capability composition without escape hatches:
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* const getDashboard = cap(z.object({}), async (_, ctx) => {
|
|
45
|
+
* const [log, projects] = await Promise.all([
|
|
46
|
+
* listAuditLog.resolve({ limit: 10 }, ctx), // mustBeAdmin runs — throws if not admin
|
|
47
|
+
* listProjects.resolve({ limit: 5 }, ctx), // mustBeAuthenticated runs
|
|
48
|
+
* ]);
|
|
49
|
+
* return { log, projects };
|
|
50
|
+
* }, 'query').guard(mustBeAuthenticated);
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* For HTTP/GraphQL/queue invocation, the execution engine uses _resolverOnly
|
|
54
|
+
* after running guards itself — guards run exactly once per external request.
|
|
55
|
+
*/
|
|
56
|
+
resolve: (input: TInput, ctx: BaseContext) => Promise<TOutput>;
|
|
57
|
+
/**
|
|
58
|
+
* Raw resolver — no guards. Used by the execution engine after it has
|
|
59
|
+
* already run guards. Also used internally by enhancers wrapping the resolver.
|
|
60
|
+
* Do not call this from application code — use resolve() instead.
|
|
61
|
+
*/
|
|
62
|
+
_resolverOnly: (input: TInput, ctx: TContext) => Promise<TOutput>;
|
|
63
|
+
/**
|
|
64
|
+
* Adds a guard to this capability.
|
|
65
|
+
*
|
|
66
|
+
* The guard must accept the capability's current TContext (or a supertype of it).
|
|
67
|
+
* Function-parameter contravariance means a guard typed for a broader context
|
|
68
|
+
* (e.g. BaseContext) is always assignable, while a guard typed for a more specific
|
|
69
|
+
* context (e.g. AppContext) requires TContext to already be at least that specific.
|
|
70
|
+
* Use `capability.withContext<AppContext>()` to start with TContext = AppContext so
|
|
71
|
+
* all AppContext guards are accepted without explicit resolver annotation.
|
|
72
|
+
*/
|
|
73
|
+
guard<G extends (ctx: TContext) => any>(g: G): Capability<TInput, TOutput, NarrowContext<TContext, G>>;
|
|
74
|
+
/**
|
|
75
|
+
* Adds an input guard that receives both validated input and context.
|
|
76
|
+
* Runs after input validation, before the resolver.
|
|
77
|
+
* Use for resource ownership checks and other input-dependent access control.
|
|
78
|
+
*/
|
|
79
|
+
inputGuard(g: InputGuard<TInput, TContext>): Capability<TInput, TOutput, TContext>;
|
|
80
|
+
enhance(e: Enhancer): Capability<TInput, TOutput, TContext>;
|
|
81
|
+
output<O>(schema: ZodSchema<O>): Capability<TInput, O, TContext>;
|
|
82
|
+
};
|
|
83
|
+
export type AnyCapability = Capability<any, any, any>;
|
|
84
|
+
/**
|
|
85
|
+
* Defines a capability — the unit of server-side logic in Capix.
|
|
86
|
+
*
|
|
87
|
+
* Wraps a resolver function with optional input validation (Zod schema), guards,
|
|
88
|
+
* and enhancers. Capabilities are the building blocks registered in createServer().
|
|
89
|
+
*
|
|
90
|
+
* @example No-input capability
|
|
91
|
+
* const ping = capability(() => ({ pong: true }));
|
|
92
|
+
*
|
|
93
|
+
* @example With Zod input schema
|
|
94
|
+
* const getUser = capability(
|
|
95
|
+
* z.object({ id: z.string() }),
|
|
96
|
+
* async (input, ctx) => db.users.find(input.id),
|
|
97
|
+
* );
|
|
98
|
+
*
|
|
99
|
+
* @example With explicit intent (overrides key-based inference in inferRoutes)
|
|
100
|
+
* const deletePost = capability(
|
|
101
|
+
* z.object({ id: z.string() }),
|
|
102
|
+
* async (input, ctx) => db.posts.delete(input.id),
|
|
103
|
+
* 'delete',
|
|
104
|
+
* );
|
|
105
|
+
*
|
|
106
|
+
* @example Adding guards and enhancers
|
|
107
|
+
* const createPost = capability(schema, resolver)
|
|
108
|
+
* .guard(mustBeAuthenticated)
|
|
109
|
+
* .enhance(withTimeout(5000));
|
|
110
|
+
*
|
|
111
|
+
* Use {@link capability.withContext} to pre-bind the context type when all
|
|
112
|
+
* capabilities in a module share the same application context.
|
|
113
|
+
*/
|
|
114
|
+
/** No-input capability. */
|
|
115
|
+
export declare function capability<TOutput, TContext extends BaseContext = BaseContext>(resolver: (input: undefined, ctx: TContext) => TOutput | Promise<TOutput>): Capability<undefined, TOutput, TContext>;
|
|
116
|
+
/** No-input capability with explicit intent. */
|
|
117
|
+
export declare function capability<TOutput, TContext extends BaseContext = BaseContext>(resolver: (input: undefined, ctx: TContext) => TOutput | Promise<TOutput>, intent: Intent): Capability<undefined, TOutput, TContext>;
|
|
118
|
+
/** With typed input schema. */
|
|
119
|
+
export declare function capability<TSchema extends ZodTypeAny, TOutput, TContext extends BaseContext = BaseContext>(schema: TSchema, resolver: Resolver<TSchema['_output'], TOutput, TContext>): Capability<TSchema['_output'], TOutput, TContext>;
|
|
120
|
+
/** With typed input schema and explicit intent. */
|
|
121
|
+
export declare function capability<TSchema extends ZodTypeAny, TOutput, TContext extends BaseContext = BaseContext>(schema: TSchema, resolver: Resolver<TSchema['_output'], TOutput, TContext>, intent: Intent): Capability<TSchema['_output'], TOutput, TContext>;
|
|
122
|
+
/** Extract the validated input type from a Capability. */
|
|
123
|
+
export type InferInput<TCap extends AnyCapability> = TCap['_input'];
|
|
124
|
+
/** Extract the resolved output type from a Capability. */
|
|
125
|
+
export type InferOutput<TCap extends AnyCapability> = TCap['_output'];
|
|
126
|
+
/** Extract the required context type from a Capability (reflects guard narrowing). */
|
|
127
|
+
export type InferContext<TCap extends AnyCapability> = TCap['_context'];
|
|
128
|
+
/** Returns true for values created by capability(). Plain objects with _capix: true return false. */
|
|
129
|
+
export declare function isCapability(value: unknown): value is AnyCapability;
|
|
130
|
+
export type CapabilityRegistry = ReadonlyMap<string, AnyCapability>;
|
|
131
|
+
/** Interface to allow recursive references (type aliases can't be circular). */
|
|
132
|
+
export interface GroupTree {
|
|
133
|
+
[key: string]: AnyCapability | GroupTree;
|
|
134
|
+
}
|
|
135
|
+
export declare function compileRegistry(tree: GroupTree, prefix?: string): CapabilityRegistry;
|
|
136
|
+
/**
|
|
137
|
+
* The type returned by `capability.withContext<TContext>()`.
|
|
138
|
+
* Identical to the `capability()` overloads but with TContext fixed.
|
|
139
|
+
*/
|
|
140
|
+
export type ScopedCapabilityFactory<TContext extends BaseContext> = {
|
|
141
|
+
<TOutput>(resolver: (input: undefined, ctx: TContext) => TOutput | Promise<TOutput>): Capability<undefined, TOutput, TContext>;
|
|
142
|
+
<TOutput>(resolver: (input: undefined, ctx: TContext) => TOutput | Promise<TOutput>, intent: Intent): Capability<undefined, TOutput, TContext>;
|
|
143
|
+
<TSchema extends ZodTypeAny, TOutput>(schema: TSchema, resolver: Resolver<TSchema['_output'], TOutput, TContext>): Capability<TSchema['_output'], TOutput, TContext>;
|
|
144
|
+
<TSchema extends ZodTypeAny, TOutput>(schema: TSchema, resolver: Resolver<TSchema['_output'], TOutput, TContext>, intent: Intent): Capability<TSchema['_output'], TOutput, TContext>;
|
|
145
|
+
};
|
|
146
|
+
export declare namespace capability {
|
|
147
|
+
/**
|
|
148
|
+
* Returns a `capability()` factory with TContext pre-bound.
|
|
149
|
+
* The resolver's `ctx` parameter is inferred as TContext without annotation,
|
|
150
|
+
* and guards typed for TContext are accepted without the `any` escape hatch.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* const appCap = capability.withContext<AppContext>();
|
|
154
|
+
* const getUser = appCap(
|
|
155
|
+
* z.object({ id: z.string() }),
|
|
156
|
+
* async (input, ctx) => { // ctx: AppContext — no annotation needed
|
|
157
|
+
* if (!ctx.user) throw Errors.Unauthorized();
|
|
158
|
+
* return db.users.find(input.id);
|
|
159
|
+
* },
|
|
160
|
+
* ).guard(mustBeAuthenticated);
|
|
161
|
+
*/
|
|
162
|
+
function withContext<TContext extends BaseContext>(): ScopedCapabilityFactory<TContext>;
|
|
163
|
+
}
|
|
164
|
+
export {};
|
|
165
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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"}
|