@mettlecast/domain-runtime 0.2.21 → 0.2.23
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/dist/primitives/api.d.ts +2 -1
- package/dist/primitives/api.js +29 -8
- package/dist/runtime/fetch.d.ts +12 -24
- package/dist/runtime/fetch.js +11 -82
- package/dist/runtime/index.d.ts +12 -1
- package/dist/runtime/index.js +12 -1
- package/dist/runtime/ky-client.d.ts +18 -0
- package/dist/runtime/ky-client.js +41 -0
- package/dist/runtime/query-parser.d.ts +6 -25
- package/dist/runtime/tracer.d.ts +35 -0
- package/dist/runtime/tracer.js +75 -0
- package/dist/runtime/typed-errors.d.ts +54 -0
- package/dist/runtime/typed-errors.js +106 -0
- package/dist/schema/helpers.d.ts +3 -3
- package/dist/schema/helpers.js +3 -3
- package/dist/types/enforcement.d.ts +7 -9
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -0
- package/dist/types/result.d.ts +91 -0
- package/dist/types/result.js +38 -0
- package/package.json +11 -2
package/dist/primitives/api.d.ts
CHANGED
|
@@ -35,7 +35,8 @@ export interface ApiDefinition extends ApiConfig {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Register an API primitive with versioned handlers.
|
|
38
|
-
* Validates that deprecated versions have a sunset date
|
|
38
|
+
* Validates that deprecated versions have a sunset date and that input/output
|
|
39
|
+
* schemas are wrapped with `.default(...)` (the Zod v4 example-data pattern).
|
|
39
40
|
* @throws {ZodError} if config is invalid
|
|
40
41
|
*/
|
|
41
42
|
export declare function defineApi(config: ApiConfig): ApiDefinition;
|
package/dist/primitives/api.js
CHANGED
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
// Native enum objects — Zod v4's recommended pattern over `z.enum([...])`.
|
|
3
|
+
// Each `as const` object becomes the source of truth; `z.nativeEnum(...)`
|
|
4
|
+
// derives the inferred string-literal union from the keys.
|
|
5
|
+
const TenancyModeLiteral = { required: 'required', none: 'none', system: 'system' };
|
|
6
|
+
const VersionStatusLiteral = { preview: 'preview', stable: 'stable', deprecated: 'deprecated', sunset: 'sunset' };
|
|
7
|
+
const AuthTypeLiteral = { jwt: 'jwt', 'api-key': 'api-key', none: 'none' };
|
|
8
|
+
const DeploymentTargetLiteral = { single: 'single', split: 'split' };
|
|
9
|
+
/**
|
|
10
|
+
* Returns true when a Zod schema has been wrapped with `.default(...)` (Zod v4
|
|
11
|
+
* pattern). The wrapped schema exposes a `_def.typeName` of `ZodDefault`.
|
|
12
|
+
* `defineApi` requires every version's `input` and `output` to be wrapped this
|
|
13
|
+
* way so that the runtime has a concrete example payload for documentation and
|
|
14
|
+
* schema-publish tooling.
|
|
15
|
+
*/
|
|
16
|
+
const hasDefault = (schema) => {
|
|
17
|
+
if (typeof schema !== 'object' || schema === null)
|
|
18
|
+
return false;
|
|
19
|
+
const def = schema._def;
|
|
20
|
+
return def?.typeName === 'ZodDefault';
|
|
21
|
+
};
|
|
2
22
|
const ApiConfigSchema = z.object({
|
|
3
23
|
id: z.string().min(1),
|
|
4
24
|
path: z.string().startsWith('/'),
|
|
5
25
|
method: z.string().optional(),
|
|
6
|
-
tenancy: z.
|
|
26
|
+
tenancy: z.nativeEnum(TenancyModeLiteral),
|
|
7
27
|
versions: z.record(z.string(), z.object({
|
|
8
|
-
status: z.
|
|
28
|
+
status: z.nativeEnum(VersionStatusLiteral),
|
|
9
29
|
sunset: z.string().optional(),
|
|
10
|
-
input: z.unknown(),
|
|
11
|
-
output: z.unknown(),
|
|
30
|
+
input: z.unknown().refine(hasDefault, { message: 'input schema must have .default()' }),
|
|
31
|
+
output: z.unknown().refine(hasDefault, { message: 'output schema must have .default()' }),
|
|
12
32
|
handler: z.function(),
|
|
13
|
-
}).refine(v => v.status !==
|
|
33
|
+
}).refine(v => v.status !== VersionStatusLiteral.deprecated || v.sunset !== undefined, {
|
|
14
34
|
message: 'sunset date is required when status is deprecated',
|
|
15
35
|
})),
|
|
16
36
|
auth: z.object({
|
|
17
|
-
type: z.
|
|
37
|
+
type: z.nativeEnum(AuthTypeLiteral),
|
|
18
38
|
roles: z.array(z.string()).optional(),
|
|
19
39
|
}).optional(),
|
|
20
40
|
rateLimit: z.object({
|
|
@@ -22,7 +42,7 @@ const ApiConfigSchema = z.object({
|
|
|
22
42
|
perApiKey: z.string().optional(),
|
|
23
43
|
perIp: z.string().optional(),
|
|
24
44
|
}).optional(),
|
|
25
|
-
deployment: z.object({ target: z.
|
|
45
|
+
deployment: z.object({ target: z.nativeEnum(DeploymentTargetLiteral) }).passthrough().optional(),
|
|
26
46
|
observability: z.object({
|
|
27
47
|
sloP99Ms: z.number().positive().optional(),
|
|
28
48
|
alertOnErrorRate: z.number().min(0).max(1).optional(),
|
|
@@ -30,7 +50,8 @@ const ApiConfigSchema = z.object({
|
|
|
30
50
|
}).refine(c => Object.keys(c.versions).length > 0, { message: 'at least one version required' });
|
|
31
51
|
/**
|
|
32
52
|
* Register an API primitive with versioned handlers.
|
|
33
|
-
* Validates that deprecated versions have a sunset date
|
|
53
|
+
* Validates that deprecated versions have a sunset date and that input/output
|
|
54
|
+
* schemas are wrapped with `.default(...)` (the Zod v4 example-data pattern).
|
|
34
55
|
* @throws {ZodError} if config is invalid
|
|
35
56
|
*/
|
|
36
57
|
export function defineApi(config) {
|
package/dist/runtime/fetch.d.ts
CHANGED
|
@@ -1,26 +1,14 @@
|
|
|
1
|
-
import type { TibFetch } from '../ctx/comms.js';
|
|
2
|
-
/** Options for the TibFetch factory. */
|
|
3
|
-
export interface TibFetchOptions {
|
|
4
|
-
/** Maximum number of retry attempts on 5xx or network errors. Defaults to 3. */
|
|
5
|
-
maxRetries?: number;
|
|
6
|
-
/** Initial backoff delay in milliseconds. Doubles each retry. Defaults to 100. */
|
|
7
|
-
initialBackoffMs?: number;
|
|
8
|
-
/**
|
|
9
|
-
* Number of consecutive failures before the circuit opens (blocks requests).
|
|
10
|
-
* Defaults to 5.
|
|
11
|
-
*/
|
|
12
|
-
circuitBreakerThreshold?: number;
|
|
13
|
-
/**
|
|
14
|
-
* How long the circuit stays open (ms) before allowing a probe request.
|
|
15
|
-
* Defaults to 30_000 (30 seconds).
|
|
16
|
-
*/
|
|
17
|
-
circuitBreakerResetMs?: number;
|
|
18
|
-
}
|
|
19
1
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
2
|
+
* @deprecated Use `createKyClient` from './ky-client.js' instead.
|
|
3
|
+
*
|
|
4
|
+
* This module is preserved for backward compatibility with code that
|
|
5
|
+
* imported `createFetch` from the runtime barrel. The underlying
|
|
6
|
+
* implementation is now backed by `ky`; the hand-rolled retry and
|
|
7
|
+
* circuit breaker have been removed.
|
|
8
|
+
*
|
|
9
|
+
* Callers passing the legacy `TibFetchOptions` fields
|
|
10
|
+
* (`initialBackoffMs`, `circuitBreakerThreshold`, `circuitBreakerResetMs`)
|
|
11
|
+
* must drop those options — they are no longer supported.
|
|
25
12
|
*/
|
|
26
|
-
export
|
|
13
|
+
export { createKyClient as createFetch } from './ky-client.js';
|
|
14
|
+
export type { KyFetchOptions as TibFetchOptions } from './ky-client.js';
|
package/dist/runtime/fetch.js
CHANGED
|
@@ -1,84 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* @deprecated Use `createKyClient` from './ky-client.js' instead.
|
|
3
|
+
*
|
|
4
|
+
* This module is preserved for backward compatibility with code that
|
|
5
|
+
* imported `createFetch` from the runtime barrel. The underlying
|
|
6
|
+
* implementation is now backed by `ky`; the hand-rolled retry and
|
|
7
|
+
* circuit breaker have been removed.
|
|
8
|
+
*
|
|
9
|
+
* Callers passing the legacy `TibFetchOptions` fields
|
|
10
|
+
* (`initialBackoffMs`, `circuitBreakerThreshold`, `circuitBreakerResetMs`)
|
|
11
|
+
* must drop those options — they are no longer supported.
|
|
7
12
|
*/
|
|
8
|
-
export
|
|
9
|
-
const { maxRetries = 3, initialBackoffMs = 100, circuitBreakerThreshold = 5, circuitBreakerResetMs = 30_000, } = options;
|
|
10
|
-
const circuits = new Map();
|
|
11
|
-
function getHost(url) {
|
|
12
|
-
try {
|
|
13
|
-
return new URL(url).host;
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
16
|
-
return url;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
function getCircuit(host) {
|
|
20
|
-
let c = circuits.get(host);
|
|
21
|
-
if (!c) {
|
|
22
|
-
c = { failures: 0, openedAt: null };
|
|
23
|
-
circuits.set(host, c);
|
|
24
|
-
}
|
|
25
|
-
return c;
|
|
26
|
-
}
|
|
27
|
-
function isOpen(circuit) {
|
|
28
|
-
if (circuit.openedAt === null)
|
|
29
|
-
return false;
|
|
30
|
-
// Half-open: allow probe after reset window.
|
|
31
|
-
if (Date.now() - circuit.openedAt >= circuitBreakerResetMs) {
|
|
32
|
-
circuit.openedAt = null; // Reset to half-open probe.
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
return true;
|
|
36
|
-
}
|
|
37
|
-
function recordSuccess(circuit) {
|
|
38
|
-
circuit.failures = 0;
|
|
39
|
-
circuit.openedAt = null;
|
|
40
|
-
}
|
|
41
|
-
function recordFailure(circuit) {
|
|
42
|
-
circuit.failures += 1;
|
|
43
|
-
if (circuit.failures >= circuitBreakerThreshold) {
|
|
44
|
-
circuit.openedAt = Date.now();
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async function sleep(ms) {
|
|
48
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
-
async fetch(url, init) {
|
|
53
|
-
const host = getHost(url);
|
|
54
|
-
const circuit = getCircuit(host);
|
|
55
|
-
if (isOpen(circuit)) {
|
|
56
|
-
throw new Error(`Circuit open for host ${host} — too many consecutive failures`);
|
|
57
|
-
}
|
|
58
|
-
let lastErr;
|
|
59
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
60
|
-
try {
|
|
61
|
-
const res = await globalThis.fetch(url, init);
|
|
62
|
-
if (res.status >= 500) {
|
|
63
|
-
recordFailure(circuit);
|
|
64
|
-
if (attempt < maxRetries) {
|
|
65
|
-
await sleep(initialBackoffMs * Math.pow(2, attempt));
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
return res; // Return the 5xx response after exhausting retries.
|
|
69
|
-
}
|
|
70
|
-
recordSuccess(circuit);
|
|
71
|
-
return res;
|
|
72
|
-
}
|
|
73
|
-
catch (err) {
|
|
74
|
-
lastErr = err;
|
|
75
|
-
recordFailure(circuit);
|
|
76
|
-
if (attempt < maxRetries) {
|
|
77
|
-
await sleep(initialBackoffMs * Math.pow(2, attempt));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
throw lastErr;
|
|
82
|
-
},
|
|
83
|
-
};
|
|
84
|
-
}
|
|
13
|
+
export { createKyClient as createFetch } from './ky-client.js';
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
export { createLogger } from './logger.js';
|
|
2
|
-
export { createTracer, noopTracer } from './tracer.js';
|
|
2
|
+
export { createTracer, noopTracer, initOtel } from './tracer.js';
|
|
3
3
|
export { createCache } from './cache.js';
|
|
4
|
+
/**
|
|
5
|
+
* @deprecated Use `createKyClient` instead. The `createFetch` name is kept
|
|
6
|
+
* as a backward-compatible alias for callers that imported it before the
|
|
7
|
+
* migration to `ky`. New code should use `createKyClient` directly.
|
|
8
|
+
*/
|
|
4
9
|
export { createFetch } from './fetch.js';
|
|
5
10
|
export type { TibFetchOptions } from './fetch.js';
|
|
11
|
+
export { createKyClient } from './ky-client.js';
|
|
12
|
+
export type { KyFetchOptions } from './ky-client.js';
|
|
6
13
|
export { checkRateLimit, clearRateLimitBuckets } from './rate-limiter.js';
|
|
14
|
+
export { ok, err, notFound } from '../types/result.js';
|
|
15
|
+
export type { AppError, Result } from '../types/result.js';
|
|
16
|
+
export { safeFetch, safeDb, wrapHandler, captureStack } from './typed-errors.js';
|
|
17
|
+
export type { SafeResult } from './typed-errors.js';
|
|
7
18
|
export { createSecrets, createEmptySecrets } from './secrets.js';
|
|
8
19
|
export type { SecretsOptions } from './secrets.js';
|
|
9
20
|
export { createIdempotency, createNoopIdempotency } from './idempotency.js';
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
// No-dep implementations
|
|
2
2
|
export { createLogger } from './logger.js';
|
|
3
|
-
export { createTracer, noopTracer } from './tracer.js';
|
|
3
|
+
export { createTracer, noopTracer, initOtel } from './tracer.js';
|
|
4
4
|
export { createCache } from './cache.js';
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated Use `createKyClient` instead. The `createFetch` name is kept
|
|
7
|
+
* as a backward-compatible alias for callers that imported it before the
|
|
8
|
+
* migration to `ky`. New code should use `createKyClient` directly.
|
|
9
|
+
*/
|
|
5
10
|
export { createFetch } from './fetch.js';
|
|
11
|
+
export { createKyClient } from './ky-client.js';
|
|
6
12
|
export { checkRateLimit, clearRateLimitBuckets } from './rate-limiter.js';
|
|
13
|
+
// ── Typed error handling ────────────────────────────────────────────────────
|
|
14
|
+
// Re-export Result types + adapters so domain handlers can import them from a
|
|
15
|
+
// single barrel. The full type definitions live in `../types/result.js`.
|
|
16
|
+
export { ok, err, notFound } from '../types/result.js';
|
|
17
|
+
export { safeFetch, safeDb, wrapHandler, captureStack } from './typed-errors.js';
|
|
7
18
|
// AWS SDK implementations
|
|
8
19
|
export { createSecrets, createEmptySecrets } from './secrets.js';
|
|
9
20
|
export { createIdempotency, createNoopIdempotency } from './idempotency.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TibFetch } from '../ctx/comms.js';
|
|
2
|
+
/** Options for the ky-backed TibFetch factory. */
|
|
3
|
+
export interface KyFetchOptions {
|
|
4
|
+
/** Maximum number of retry attempts on retriable status codes. Defaults to 3. */
|
|
5
|
+
maxRetries?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Creates an HTTP client backed by ky with built-in retry on 503 and 429
|
|
9
|
+
* status codes. Replaces the hand-rolled retry + circuit breaker previously
|
|
10
|
+
* implemented in `createFetch`.
|
|
11
|
+
*
|
|
12
|
+
* Retries use ky's exponential backoff. The `beforeRetry` hook is wired up
|
|
13
|
+
* to allow future logging/metrics extensions without modifying the call site.
|
|
14
|
+
*
|
|
15
|
+
* @param options - Configuration for retry behaviour.
|
|
16
|
+
* @returns A TibFetch instance backed by ky.
|
|
17
|
+
*/
|
|
18
|
+
export declare function createKyClient(options?: KyFetchOptions): TibFetch;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
/**
|
|
3
|
+
* Creates an HTTP client backed by ky with built-in retry on 503 and 429
|
|
4
|
+
* status codes. Replaces the hand-rolled retry + circuit breaker previously
|
|
5
|
+
* implemented in `createFetch`.
|
|
6
|
+
*
|
|
7
|
+
* Retries use ky's exponential backoff. The `beforeRetry` hook is wired up
|
|
8
|
+
* to allow future logging/metrics extensions without modifying the call site.
|
|
9
|
+
*
|
|
10
|
+
* @param options - Configuration for retry behaviour.
|
|
11
|
+
* @returns A TibFetch instance backed by ky.
|
|
12
|
+
*/
|
|
13
|
+
export function createKyClient(options = {}) {
|
|
14
|
+
const { maxRetries = 3 } = options;
|
|
15
|
+
const instance = ky.create({
|
|
16
|
+
retry: {
|
|
17
|
+
limit: maxRetries,
|
|
18
|
+
methods: ['get', 'post', 'put', 'patch', 'delete'],
|
|
19
|
+
statusCodes: [503, 429],
|
|
20
|
+
},
|
|
21
|
+
hooks: {
|
|
22
|
+
beforeRetry: [
|
|
23
|
+
async () => {
|
|
24
|
+
// Placeholder hook for future logging/metrics. Ky performs the retry.
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
async fetch(url, init) {
|
|
31
|
+
// `RequestInit` (the public TibFetch signature) is a strict subset of
|
|
32
|
+
// ky's `Options`. The double-cast `as unknown as KyOptions` is the
|
|
33
|
+
// canonical TypeScript pattern for "I know these types are related but
|
|
34
|
+
// TS doesn't" — it forces a deliberate acknowledgement that the
|
|
35
|
+
// fields on `init` are a subset of what ky accepts, and is safer than
|
|
36
|
+
// a direct `as KyOptions` cast (which TS allows without the
|
|
37
|
+
// intermediate acknowledgement).
|
|
38
|
+
return instance(url, init);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -6,32 +6,13 @@ export declare const ParsedQuerySchema: z.ZodObject<{
|
|
|
6
6
|
limit: z.ZodDefault<z.ZodNumber>;
|
|
7
7
|
sort: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
8
8
|
field: z.ZodString;
|
|
9
|
-
direction: z.ZodEnum<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
},
|
|
14
|
-
field: string;
|
|
15
|
-
direction: "asc" | "desc";
|
|
16
|
-
}>, "many">>;
|
|
9
|
+
direction: z.ZodEnum<{
|
|
10
|
+
asc: "asc";
|
|
11
|
+
desc: "desc";
|
|
12
|
+
}>;
|
|
13
|
+
}, z.core.$strip>>>;
|
|
17
14
|
filter: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
18
|
-
},
|
|
19
|
-
sort: {
|
|
20
|
-
field: string;
|
|
21
|
-
direction: "asc" | "desc";
|
|
22
|
-
}[];
|
|
23
|
-
filter: Record<string, string>;
|
|
24
|
-
limit: number;
|
|
25
|
-
cursor?: string | undefined;
|
|
26
|
-
}, {
|
|
27
|
-
sort?: {
|
|
28
|
-
field: string;
|
|
29
|
-
direction: "asc" | "desc";
|
|
30
|
-
}[] | undefined;
|
|
31
|
-
filter?: Record<string, string> | undefined;
|
|
32
|
-
cursor?: string | undefined;
|
|
33
|
-
limit?: number | undefined;
|
|
34
|
-
}>;
|
|
15
|
+
}, z.core.$strip>;
|
|
35
16
|
export type ParsedQuery = z.infer<typeof ParsedQuerySchema>;
|
|
36
17
|
/**
|
|
37
18
|
* Parses standard TIB list query parameters from a raw query string map.
|
package/dist/runtime/tracer.d.ts
CHANGED
|
@@ -11,3 +11,38 @@ export declare function createTracer(segmentName: string): Tracer;
|
|
|
11
11
|
* `startSegment` calls `fn()` directly. `putAnnotation` is a no-op.
|
|
12
12
|
*/
|
|
13
13
|
export declare const noopTracer: Tracer;
|
|
14
|
+
/**
|
|
15
|
+
* Initialises OpenTelemetry tracing for the process.
|
|
16
|
+
*
|
|
17
|
+
* **Idempotent** — safe to call at module scope (top of a Lambda handler
|
|
18
|
+
* file) and safe to call repeatedly. The module-level `initialised` flag
|
|
19
|
+
* ensures only the first call has any effect.
|
|
20
|
+
*
|
|
21
|
+
* **Opt-in by design.** This function is NOT called automatically by the
|
|
22
|
+
* runtime; domain handler templates must call `initOtel()` at module
|
|
23
|
+
* scope if they want OTel-exported spans. The `tib doctor` gate
|
|
24
|
+
* (`checkOtelInitInLambdas`) enforces this convention by scanning
|
|
25
|
+
* `domains/.../api/-.ts` for an `initOtel(...)` call.
|
|
26
|
+
*
|
|
27
|
+
* Behaviour:
|
|
28
|
+
* - When `OTEL_EXPORTER_OTLP_ENDPOINT` is set, attaches a
|
|
29
|
+
* `BatchSpanProcessor` with an `OTLPTraceExporter` pointed at that
|
|
30
|
+
* URL.
|
|
31
|
+
* - Otherwise (no endpoint configured), attaches a `SimpleSpanProcessor`
|
|
32
|
+
* with a `ConsoleSpanExporter` so spans are visible during local
|
|
33
|
+
* development.
|
|
34
|
+
* - Sampling is `TraceIdRatioBasedSampler` driven by
|
|
35
|
+
* `OTEL TRACES_SAMPLER_ARG` (default `0.01` = 1%). Invalid values
|
|
36
|
+
* fall back to the 1% default.
|
|
37
|
+
* - The provider is registered globally via `provider.register()`,
|
|
38
|
+
* which sets up the global tracer, context propagation, and (when
|
|
39
|
+
* instrumentations are added) auto-instrumentation.
|
|
40
|
+
*
|
|
41
|
+
* **Auto-instrumentation caveat**: this initialiser does NOT install
|
|
42
|
+
* auto-instrumentations (HTTP, Express, etc.). If you need
|
|
43
|
+
* auto-instrumentation, add the relevant `@opentelemetry/instrumentation-*`
|
|
44
|
+
* packages and call `registerInstrumentations({ instrumentations: [...] })`
|
|
45
|
+
* after `initOtel()`. The basic tracer + context-propagation setup
|
|
46
|
+
* provided here works for manually-created spans.
|
|
47
|
+
*/
|
|
48
|
+
export declare function initOtel(): void;
|
package/dist/runtime/tracer.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { Tracer as PowertoolsTracer } from '@aws-lambda-powertools/tracer';
|
|
2
|
+
// OTel deps (`@opentelemetry/*`) are in packages/domain-runtime/package.json
|
|
3
|
+
// (added in Wave 2 of the scaffolder modernization, issue #3831).
|
|
4
|
+
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
|
5
|
+
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
6
|
+
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
|
|
7
|
+
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
|
|
8
|
+
import { BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor, TraceIdRatioBasedSampler, } from '@opentelemetry/sdk-trace-node';
|
|
2
9
|
/**
|
|
3
10
|
* Creates an AWS X-Ray compatible Tracer using Powertools Tracer.
|
|
4
11
|
* Degrades gracefully when called outside a Lambda context (no active segment).
|
|
@@ -51,3 +58,71 @@ export const noopTracer = {
|
|
|
51
58
|
// no-op
|
|
52
59
|
},
|
|
53
60
|
};
|
|
61
|
+
let initialised = false;
|
|
62
|
+
/**
|
|
63
|
+
* Initialises OpenTelemetry tracing for the process.
|
|
64
|
+
*
|
|
65
|
+
* **Idempotent** — safe to call at module scope (top of a Lambda handler
|
|
66
|
+
* file) and safe to call repeatedly. The module-level `initialised` flag
|
|
67
|
+
* ensures only the first call has any effect.
|
|
68
|
+
*
|
|
69
|
+
* **Opt-in by design.** This function is NOT called automatically by the
|
|
70
|
+
* runtime; domain handler templates must call `initOtel()` at module
|
|
71
|
+
* scope if they want OTel-exported spans. The `tib doctor` gate
|
|
72
|
+
* (`checkOtelInitInLambdas`) enforces this convention by scanning
|
|
73
|
+
* `domains/.../api/-.ts` for an `initOtel(...)` call.
|
|
74
|
+
*
|
|
75
|
+
* Behaviour:
|
|
76
|
+
* - When `OTEL_EXPORTER_OTLP_ENDPOINT` is set, attaches a
|
|
77
|
+
* `BatchSpanProcessor` with an `OTLPTraceExporter` pointed at that
|
|
78
|
+
* URL.
|
|
79
|
+
* - Otherwise (no endpoint configured), attaches a `SimpleSpanProcessor`
|
|
80
|
+
* with a `ConsoleSpanExporter` so spans are visible during local
|
|
81
|
+
* development.
|
|
82
|
+
* - Sampling is `TraceIdRatioBasedSampler` driven by
|
|
83
|
+
* `OTEL TRACES_SAMPLER_ARG` (default `0.01` = 1%). Invalid values
|
|
84
|
+
* fall back to the 1% default.
|
|
85
|
+
* - The provider is registered globally via `provider.register()`,
|
|
86
|
+
* which sets up the global tracer, context propagation, and (when
|
|
87
|
+
* instrumentations are added) auto-instrumentation.
|
|
88
|
+
*
|
|
89
|
+
* **Auto-instrumentation caveat**: this initialiser does NOT install
|
|
90
|
+
* auto-instrumentations (HTTP, Express, etc.). If you need
|
|
91
|
+
* auto-instrumentation, add the relevant `@opentelemetry/instrumentation-*`
|
|
92
|
+
* packages and call `registerInstrumentations({ instrumentations: [...] })`
|
|
93
|
+
* after `initOtel()`. The basic tracer + context-propagation setup
|
|
94
|
+
* provided here works for manually-created spans.
|
|
95
|
+
*/
|
|
96
|
+
export function initOtel() {
|
|
97
|
+
if (initialised)
|
|
98
|
+
return;
|
|
99
|
+
initialised = true;
|
|
100
|
+
const endpoint = process.env['OTEL_EXPORTER_OTLP_ENDPOINT'];
|
|
101
|
+
const samplerArg = process.env['OTEL_TRACES_SAMPLER_ARG'] ?? '0.01';
|
|
102
|
+
const parsed = Number.parseFloat(samplerArg);
|
|
103
|
+
const ratio = Number.isNaN(parsed) ? 0.01 : parsed;
|
|
104
|
+
const sampler = new TraceIdRatioBasedSampler(ratio);
|
|
105
|
+
const provider = new NodeTracerProvider({ sampler });
|
|
106
|
+
if (endpoint) {
|
|
107
|
+
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({ url: endpoint })));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
|
|
111
|
+
}
|
|
112
|
+
// Use provider.register() rather than trace.setGlobalTracerProvider() so
|
|
113
|
+
// that context propagation is set up correctly and any future
|
|
114
|
+
// auto-instrumentation (HTTP, Express, etc.) will work without additional
|
|
115
|
+
// wiring. The two calls are equivalent for the simple case of "set the
|
|
116
|
+
// global provider" but `register()` also installs the W3C TraceContext
|
|
117
|
+
// propagators and registers the provider with the metrics + context
|
|
118
|
+
// managers — the recommended OTel SDK pattern.
|
|
119
|
+
provider.register();
|
|
120
|
+
// Register auto-instrumentation for outbound HTTP calls. Every
|
|
121
|
+
// `ctx.fetch()` (which uses Node's `http`/`https` modules under ky)
|
|
122
|
+
// will produce a child span automatically. Inbound HTTP is handled by
|
|
123
|
+
// API Gateway (which creates its own span), so this instrumentation
|
|
124
|
+
// only covers the outbound (service-to-service) call path.
|
|
125
|
+
registerInstrumentations({
|
|
126
|
+
instrumentations: [new HttpInstrumentation()],
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime adapters that convert throw-able operations into typed
|
|
3
|
+
* `Result<T, AppError>` values. Domain handlers import these (or the
|
|
4
|
+
* lower-level `ok`/`err` constructors) and return `Result` instead of
|
|
5
|
+
* throwing, so every error path is explicit in the handler's return type.
|
|
6
|
+
*/
|
|
7
|
+
import { createKyClient } from './ky-client.js';
|
|
8
|
+
import type { AppError, InternalError, Result } from '../types/result.js';
|
|
9
|
+
export type { AppError, Result } from '../types/result.js';
|
|
10
|
+
export type SafeResult<T> = Result<T, AppError>;
|
|
11
|
+
/**
|
|
12
|
+
* Create an `internal` error from a caught throwable. Extracts a
|
|
13
|
+
* best-effort message and stack trace; the caller should already have
|
|
14
|
+
* logged the full error before returning the Result to the API gateway.
|
|
15
|
+
*/
|
|
16
|
+
export declare function captureStack(err: unknown, traceId: string): InternalError;
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a `TibFetch` instance so that every `fetch()` call returns a
|
|
19
|
+
* `Result<Response, AppError>` instead of throwing. Network errors,
|
|
20
|
+
* unexpected status codes, and JSON parse failures are all converted to
|
|
21
|
+
* typed errors.
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* const http = safeFetch(ctx.fetch);
|
|
25
|
+
* const res = await http.get('https://api.example.com/users');
|
|
26
|
+
* if (!res.ok) return res; // res.error is typed AppError
|
|
27
|
+
* const users = await res.json(); // res.value.json()
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export interface SafeFetch {
|
|
31
|
+
fetch(url: string, init?: RequestInit): Promise<SafeResult<Response>>;
|
|
32
|
+
}
|
|
33
|
+
export declare function safeFetch(client: ReturnType<typeof createKyClient>): SafeFetch;
|
|
34
|
+
/**
|
|
35
|
+
* Stub — wraps the existing db client so that common query errors are
|
|
36
|
+
* converted to typed errors. This is a skeleton for future expansion;
|
|
37
|
+
* the current version only catches unexpected throws.
|
|
38
|
+
*/
|
|
39
|
+
export interface SafeDb {
|
|
40
|
+
query(domainId: string, sql: string, params?: unknown[]): Promise<SafeResult<unknown[]>>;
|
|
41
|
+
}
|
|
42
|
+
export declare function safeDb(db: {
|
|
43
|
+
query: (domainId: string, sql: string, params?: unknown[]) => Promise<unknown[]>;
|
|
44
|
+
}): SafeDb;
|
|
45
|
+
/**
|
|
46
|
+
* Wraps a domain handler that returns `Result<T, AppError>` in a
|
|
47
|
+
* standard API Gateway lambda wrapper. Success results become 2xx
|
|
48
|
+
* responses; failure results become the appropriate HTTP status code
|
|
49
|
+
* (4xx/5xx) with structured error bodies.
|
|
50
|
+
*
|
|
51
|
+
* Any *unexpected* throw inside the handler is caught and converted to
|
|
52
|
+
* a 500 with the trace ID — the page never crashes.
|
|
53
|
+
*/
|
|
54
|
+
export declare function wrapHandler<T>(handler: (input: unknown, ctx: unknown) => Promise<Result<T, AppError>>): (input: unknown, ctx: unknown) => Promise<unknown>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime adapters that convert throw-able operations into typed
|
|
3
|
+
* `Result<T, AppError>` values. Domain handlers import these (or the
|
|
4
|
+
* lower-level `ok`/`err` constructors) and return `Result` instead of
|
|
5
|
+
* throwing, so every error path is explicit in the handler's return type.
|
|
6
|
+
*/
|
|
7
|
+
import { ok, err } from '../types/result.js';
|
|
8
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
9
|
+
/**
|
|
10
|
+
* Create an `internal` error from a caught throwable. Extracts a
|
|
11
|
+
* best-effort message and stack trace; the caller should already have
|
|
12
|
+
* logged the full error before returning the Result to the API gateway.
|
|
13
|
+
*/
|
|
14
|
+
export function captureStack(err, traceId) {
|
|
15
|
+
if (err instanceof Error) {
|
|
16
|
+
return {
|
|
17
|
+
kind: 'internal',
|
|
18
|
+
traceId,
|
|
19
|
+
message: err.message,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
kind: 'internal',
|
|
24
|
+
traceId,
|
|
25
|
+
message: String(err),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function safeFetch(client) {
|
|
29
|
+
return {
|
|
30
|
+
async fetch(url, init) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await client.fetch(url, init);
|
|
33
|
+
return ok(response);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
37
|
+
return err({
|
|
38
|
+
kind: 'external_service',
|
|
39
|
+
service: url,
|
|
40
|
+
status: 0,
|
|
41
|
+
body: message,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function safeDb(db) {
|
|
48
|
+
return {
|
|
49
|
+
async query(domainId, sql, params) {
|
|
50
|
+
try {
|
|
51
|
+
const rows = await db.query(domainId, sql, params);
|
|
52
|
+
return ok(rows);
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
56
|
+
return err({
|
|
57
|
+
kind: 'external_service',
|
|
58
|
+
service: 'database',
|
|
59
|
+
status: 0,
|
|
60
|
+
body: message,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// ── wrapHandler ─────────────────────────────────────────────────────────────
|
|
67
|
+
/**
|
|
68
|
+
* Wraps a domain handler that returns `Result<T, AppError>` in a
|
|
69
|
+
* standard API Gateway lambda wrapper. Success results become 2xx
|
|
70
|
+
* responses; failure results become the appropriate HTTP status code
|
|
71
|
+
* (4xx/5xx) with structured error bodies.
|
|
72
|
+
*
|
|
73
|
+
* Any *unexpected* throw inside the handler is caught and converted to
|
|
74
|
+
* a 500 with the trace ID — the page never crashes.
|
|
75
|
+
*/
|
|
76
|
+
export function wrapHandler(handler) {
|
|
77
|
+
return async (input, ctx) => {
|
|
78
|
+
try {
|
|
79
|
+
const result = await handler(input, ctx);
|
|
80
|
+
if (result.ok)
|
|
81
|
+
return { statusCode: 200, body: JSON.stringify(result.value) };
|
|
82
|
+
return errorToResponse(result.error);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
86
|
+
return errorToResponse({ kind: 'internal', traceId: '', message });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ── HTTP mapping ────────────────────────────────────────────────────────────
|
|
91
|
+
function errorToResponse(error) {
|
|
92
|
+
const statusMap = {
|
|
93
|
+
not_found: 404,
|
|
94
|
+
validation: 400,
|
|
95
|
+
conflict: 409,
|
|
96
|
+
unauthorized: 401,
|
|
97
|
+
forbidden: 403,
|
|
98
|
+
rate_limited: 429,
|
|
99
|
+
external_service: 502,
|
|
100
|
+
internal: 500,
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
statusCode: statusMap[error.kind],
|
|
104
|
+
body: JSON.stringify({ error }),
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/schema/helpers.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
/** UUID v4/v7 string schema. */
|
|
3
|
-
export declare const uuid: () => z.
|
|
3
|
+
export declare const uuid: () => z.ZodUUID;
|
|
4
4
|
/** ISO 8601 date-time string schema. */
|
|
5
|
-
export declare const isoDate: () => z.
|
|
5
|
+
export declare const isoDate: () => z.ZodISODateTime;
|
|
6
6
|
/**
|
|
7
7
|
* Semver version string schema (e.g. "1.2.3", "^1.0.0", "~2.1").
|
|
8
8
|
* Accepts a loose superset sufficient for package version ranges.
|
|
@@ -14,4 +14,4 @@ export declare const semver: () => z.ZodString;
|
|
|
14
14
|
*/
|
|
15
15
|
export declare const cronExpression: () => z.ZodString;
|
|
16
16
|
/** UUID v7 tenant-id string schema (same shape as uuid but named for clarity). */
|
|
17
|
-
export declare const tenantId: () => z.
|
|
17
|
+
export declare const tenantId: () => z.ZodUUID;
|
package/dist/schema/helpers.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
/** UUID v4/v7 string schema. */
|
|
3
|
-
export const uuid = () => z.
|
|
3
|
+
export const uuid = () => z.uuid();
|
|
4
4
|
/** ISO 8601 date-time string schema. */
|
|
5
|
-
export const isoDate = () => z.
|
|
5
|
+
export const isoDate = () => z.iso.datetime({ offset: true });
|
|
6
6
|
/**
|
|
7
7
|
* Semver version string schema (e.g. "1.2.3", "^1.0.0", "~2.1").
|
|
8
8
|
* Accepts a loose superset sufficient for package version ranges.
|
|
@@ -14,4 +14,4 @@ export const semver = () => z.string().regex(/^[\^~]?\d+\.\d+\.\d+(?:-[\w.]+)?(?
|
|
|
14
14
|
*/
|
|
15
15
|
export const cronExpression = () => z.string().regex(/^(?:cron\([^)]+\)|rate\(\d+ (?:minute|minutes|hour|hours|day|days)\))$/, 'Must be a valid cron(…) or rate(…) expression');
|
|
16
16
|
/** UUID v7 tenant-id string schema (same shape as uuid but named for clarity). */
|
|
17
|
-
export const tenantId = () => z.
|
|
17
|
+
export const tenantId = () => z.uuid();
|
|
@@ -25,12 +25,10 @@ export interface DomainModuleConfig {
|
|
|
25
25
|
* Used by the CLI config loader to validate `.mc/domain-module.config.ts` exports.
|
|
26
26
|
*/
|
|
27
27
|
export declare const DomainModuleConfigSchema: z.ZodObject<{
|
|
28
|
-
mode: z.ZodEnum<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
legacyAllowlist?: string[] | undefined;
|
|
36
|
-
}>;
|
|
28
|
+
mode: z.ZodEnum<{
|
|
29
|
+
optional: "optional";
|
|
30
|
+
enforced: "enforced";
|
|
31
|
+
strict: "strict";
|
|
32
|
+
}>;
|
|
33
|
+
legacyAllowlist: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
34
|
+
}, z.core.$strip>;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -4,3 +4,5 @@ export type { AuthConfig, RateLimitConfig, TenancyMode } from './auth.js';
|
|
|
4
4
|
export type { DeploymentConfig, ObservabilityConfig, EnforcementMode } from './deployment.js';
|
|
5
5
|
export type { DomainModuleConfig } from './enforcement.js';
|
|
6
6
|
export { DomainModuleConfigSchema } from './enforcement.js';
|
|
7
|
+
export type { AppError, Result, NotFoundError, ValidationError, ConflictError, UnauthorizedError, ForbiddenError, RateLimitedError, ExternalServiceError, InternalError } from './result.js';
|
|
8
|
+
export { ok, err, notFound } from './result.js';
|
package/dist/types/index.js
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error handling for domain handlers.
|
|
3
|
+
*
|
|
4
|
+
* The `AppError` union replaces bare `throw` statements with structured,
|
|
5
|
+
* categorised errors that every handler can match against. Result<T>
|
|
6
|
+
* encodes the success-or-failure outcome so callers are forced to check
|
|
7
|
+
* before using the value — no unhandled promise rejections, no catch-all
|
|
8
|
+
* "Internal Server Error" pages.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* function getTenant(id: string): Result<Tenant, AppError> {
|
|
14
|
+
* const row = await db.query(...);
|
|
15
|
+
* if (!row) return err({ kind: 'not_found', resource: 'tenant', id });
|
|
16
|
+
* return ok(row);
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/** Every possible error kind a domain handler can return. */
|
|
21
|
+
export type AppError = NotFoundError | ValidationError | ConflictError | UnauthorizedError | ForbiddenError | RateLimitedError | ExternalServiceError | InternalError;
|
|
22
|
+
/** The requested resource does not exist. */
|
|
23
|
+
export interface NotFoundError {
|
|
24
|
+
kind: 'not_found';
|
|
25
|
+
resource: string;
|
|
26
|
+
id: string;
|
|
27
|
+
}
|
|
28
|
+
/** Input validation failed. Field-level errors for form highlighting. */
|
|
29
|
+
export interface ValidationError {
|
|
30
|
+
kind: 'validation';
|
|
31
|
+
fieldErrors: Record<string, string[]>;
|
|
32
|
+
message: string;
|
|
33
|
+
}
|
|
34
|
+
/** The operation conflicts with the current state (e.g. duplicate). */
|
|
35
|
+
export interface ConflictError {
|
|
36
|
+
kind: 'conflict';
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
/** The caller must authenticate first. */
|
|
40
|
+
export interface UnauthorizedError {
|
|
41
|
+
kind: 'unauthorized';
|
|
42
|
+
message: string;
|
|
43
|
+
}
|
|
44
|
+
/** The caller is authenticated but lacks permission. */
|
|
45
|
+
export interface ForbiddenError {
|
|
46
|
+
kind: 'forbidden';
|
|
47
|
+
message: string;
|
|
48
|
+
}
|
|
49
|
+
/** Rate limit exceeded. The caller should retry after the specified
|
|
50
|
+
* number of seconds. */
|
|
51
|
+
export interface RateLimitedError {
|
|
52
|
+
kind: 'rate_limited';
|
|
53
|
+
retryAfterSeconds: number;
|
|
54
|
+
}
|
|
55
|
+
/** An external service returned an error. The handler should surface
|
|
56
|
+
* the original status and a sanitised summary — do NOT log raw
|
|
57
|
+
* response bodies from third-party APIs. */
|
|
58
|
+
export interface ExternalServiceError {
|
|
59
|
+
kind: 'external_service';
|
|
60
|
+
service: string;
|
|
61
|
+
status: number;
|
|
62
|
+
body: unknown;
|
|
63
|
+
}
|
|
64
|
+
/** Something unexpected happened. The `traceId` is a correlation
|
|
65
|
+
* identifier for the current request — include it in user-facing
|
|
66
|
+
* error messages so support can look up the details in CloudWatch. */
|
|
67
|
+
export interface InternalError {
|
|
68
|
+
kind: 'internal';
|
|
69
|
+
traceId: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}
|
|
72
|
+
/** Discriminated union: either `ok` with a value, or not-ok with an error. */
|
|
73
|
+
export type Result<T, E = AppError> = {
|
|
74
|
+
ok: true;
|
|
75
|
+
value: T;
|
|
76
|
+
} | {
|
|
77
|
+
ok: false;
|
|
78
|
+
error: E;
|
|
79
|
+
};
|
|
80
|
+
/** Create a success Result. */
|
|
81
|
+
export declare function ok<T>(value: T): Result<T, never>;
|
|
82
|
+
/** Create a failure Result. */
|
|
83
|
+
export declare function err<E extends AppError>(error: E): Result<never, E>;
|
|
84
|
+
/**
|
|
85
|
+
* Convenience: return a pre-built 404-style error for a missing resource.
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* if (!row) return notFound('tenant', id);
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export declare function notFound(resource: string, id: string): Result<never, AppError>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed error handling for domain handlers.
|
|
3
|
+
*
|
|
4
|
+
* The `AppError` union replaces bare `throw` statements with structured,
|
|
5
|
+
* categorised errors that every handler can match against. Result<T>
|
|
6
|
+
* encodes the success-or-failure outcome so callers are forced to check
|
|
7
|
+
* before using the value — no unhandled promise rejections, no catch-all
|
|
8
|
+
* "Internal Server Error" pages.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* function getTenant(id: string): Result<Tenant, AppError> {
|
|
14
|
+
* const row = await db.query(...);
|
|
15
|
+
* if (!row) return err({ kind: 'not_found', resource: 'tenant', id });
|
|
16
|
+
* return ok(row);
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
// ── Constructors ────────────────────────────────────────────────────────────
|
|
21
|
+
/** Create a success Result. */
|
|
22
|
+
export function ok(value) {
|
|
23
|
+
return { ok: true, value };
|
|
24
|
+
}
|
|
25
|
+
/** Create a failure Result. */
|
|
26
|
+
export function err(error) {
|
|
27
|
+
return { ok: false, error };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Convenience: return a pre-built 404-style error for a missing resource.
|
|
31
|
+
*
|
|
32
|
+
* ```ts
|
|
33
|
+
* if (!row) return notFound('tenant', id);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function notFound(resource, id) {
|
|
37
|
+
return err({ kind: 'not_found', resource, id });
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mettlecast/domain-runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"registry": "https://registry.npmjs.org",
|
|
@@ -58,10 +58,19 @@
|
|
|
58
58
|
"@aws-lambda-powertools/logger": "^2.0.0",
|
|
59
59
|
"@aws-lambda-powertools/metrics": "^2.0.0",
|
|
60
60
|
"@aws-lambda-powertools/tracer": "^2.0.0",
|
|
61
|
+
"@opentelemetry/api": "~1.8.0",
|
|
62
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.50.0",
|
|
63
|
+
"@opentelemetry/instrumentation": "^0.50.0",
|
|
64
|
+
"@opentelemetry/instrumentation-http": "^0.50.0",
|
|
65
|
+
"@opentelemetry/sdk-trace-node": "^1.0.0",
|
|
61
66
|
"drizzle-orm": "*",
|
|
67
|
+
"ky": "^1.7.0",
|
|
62
68
|
"pg": "^8.0.0",
|
|
63
69
|
"semver": "^7.0.0",
|
|
64
|
-
"zod": "^
|
|
70
|
+
"zod": "^4.0.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"zod": "^4.0.0"
|
|
65
74
|
},
|
|
66
75
|
"devDependencies": {
|
|
67
76
|
"@types/aws-lambda": "^8.10.0",
|