@krak-stack/registry 0.1.17 → 0.1.19
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/README.md +5 -0
- package/dist/lib/httpapi-cli.d.ts +2 -1
- package/dist/lib/httpapi-cli.js +65 -15
- package/dist/lib/httpapi-client.d.ts +2 -1
- package/dist/lib/httpapi-client.js +45 -3
- package/dist/lib/httpapi-mcp.js +52 -11
- package/dist/lib/httpapi-toolkit.js +52 -11
- package/dist/services/health/api.builder.d.ts +66 -0
- package/dist/services/health/api.group.d.ts +58 -0
- package/dist/services/health/index.d.ts +253 -0
- package/dist/services/health/index.js +186 -0
- package/dist/services/health/schema.d.ts +50 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -33,6 +33,11 @@ import { Query } from "@krak-stack/registry/query";
|
|
|
33
33
|
import { createSeo } from "@krak-stack/registry/seo";
|
|
34
34
|
import { FileExtractionService } from "@krak-stack/registry/service-file-extraction";
|
|
35
35
|
import { FileExtractedTextSchema } from "@krak-stack/registry/service-file-extraction/schema";
|
|
36
|
+
import {
|
|
37
|
+
HealthApiGroup,
|
|
38
|
+
healthHandler,
|
|
39
|
+
HealthService,
|
|
40
|
+
} from "@krak-stack/registry/service-health";
|
|
36
41
|
import { NotificationService } from "@krak-stack/registry/service-notification";
|
|
37
42
|
```
|
|
38
43
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Context, Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect";
|
|
1
|
+
import { Cause, Context, Effect, FileSystem, Layer, Path, Stdio, Terminal } from "effect";
|
|
2
2
|
import { Command } from "effect/unstable/cli";
|
|
3
3
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
4
4
|
import { ApiClient, type ApiClientService } from "./httpapi-client.js";
|
|
@@ -19,5 +19,6 @@ export declare const printHttpApiCliResult: (response: unknown, operation: HttpA
|
|
|
19
19
|
export declare const makeHttpApiCliCommand: () => Effect.Effect<Command.Command<string, {}, {}, unknown, never>, never, ApiClient | HttpApiSpec>;
|
|
20
20
|
export declare const httpApiCliEnvironmentLayer: (args: ReadonlyArray<string>) => Layer.Layer<ChildProcessSpawner | FileSystem.FileSystem | Path.Path | Stdio.Stdio | Terminal.Terminal, never, never>;
|
|
21
21
|
export declare const httpApiCli: (args?: string[]) => Effect.Effect<void, unknown, HttpApiCli | Command.Environment>;
|
|
22
|
+
export declare const formatHttpApiCliCause: <E>(cause: Cause.Cause<E>) => string;
|
|
22
23
|
export declare const runHttpApiCli: <E>(layer: Layer.Layer<HttpApiCli, E>, args?: string[]) => void;
|
|
23
24
|
export {};
|
package/dist/lib/httpapi-cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// ../../src/lib/httpapi-cli.ts
|
|
2
2
|
import {
|
|
3
|
+
Cause,
|
|
3
4
|
Console,
|
|
4
5
|
Context as Context3,
|
|
5
6
|
Effect as Effect3,
|
|
7
|
+
Exit,
|
|
6
8
|
FileSystem,
|
|
7
9
|
Layer as Layer3,
|
|
8
10
|
Path,
|
|
@@ -15,13 +17,22 @@ import { Command, Flag } from "effect/unstable/cli";
|
|
|
15
17
|
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
16
18
|
|
|
17
19
|
// ../../src/lib/httpapi-client.ts
|
|
18
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
20
|
+
import { Context, Effect, Layer, Schema, SchemaTransformation } from "effect";
|
|
19
21
|
import { HttpClient } from "effect/unstable/http";
|
|
20
22
|
import {
|
|
21
|
-
|
|
23
|
+
HttpApi,
|
|
24
|
+
HttpApiClient as EffectHttpApiClient,
|
|
25
|
+
OpenApi
|
|
22
26
|
} from "effect/unstable/httpapi";
|
|
27
|
+
var UndefinedFromNull = Schema.Null.pipe(Schema.decodeTo(Schema.Undefined, SchemaTransformation.transform({
|
|
28
|
+
decode: () => {
|
|
29
|
+
return;
|
|
30
|
+
},
|
|
31
|
+
encode: () => null
|
|
32
|
+
})));
|
|
23
33
|
var HttpApiOperationResultValue = Schema.suspend(() => Schema.Union([
|
|
24
34
|
Schema.Null,
|
|
35
|
+
UndefinedFromNull,
|
|
25
36
|
Schema.String,
|
|
26
37
|
Schema.Number,
|
|
27
38
|
Schema.Boolean,
|
|
@@ -35,6 +46,37 @@ var HttpApiOperationResult = Schema.Union([
|
|
|
35
46
|
Schema.Undefined
|
|
36
47
|
]).annotate({ identifier: "HttpApiOperationResult" });
|
|
37
48
|
var encodeHttpApiOperationResult = Effect.fn("HttpApiClient.encodeOperationResult")((result) => Schema.encodeUnknownEffect(HttpApiOperationResult)(result).pipe(Effect.map((encoded) => encoded ?? null), Effect.mapError((cause) => new Error("HTTP API result is not serializable", { cause }))));
|
|
49
|
+
var reflectedSchema = (schema) => Schema.make(schema.ast);
|
|
50
|
+
var operationResultSchemas = (api) => {
|
|
51
|
+
const schemas = new Map;
|
|
52
|
+
HttpApi.reflect(api, {
|
|
53
|
+
onGroup: () => {
|
|
54
|
+
return;
|
|
55
|
+
},
|
|
56
|
+
onEndpoint: ({ endpoint, group, successes }) => {
|
|
57
|
+
const operationId = Context.getOrElse(endpoint.annotations, OpenApi.Identifier, () => group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`);
|
|
58
|
+
const operationSchemas = Array.from(successes.values()).flat().map(reflectedSchema);
|
|
59
|
+
if (operationSchemas.length === 1 && operationSchemas[0]) {
|
|
60
|
+
schemas.set(operationId, operationSchemas[0]);
|
|
61
|
+
} else if (operationSchemas.length > 1) {
|
|
62
|
+
schemas.set(operationId, Schema.Union(operationSchemas));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return schemas;
|
|
67
|
+
};
|
|
68
|
+
var makeHttpApiOperationResultEncoder = (api) => {
|
|
69
|
+
const schemas = operationResultSchemas(api);
|
|
70
|
+
return Effect.fn("HttpApiClient.encodeOperationResultWithSchema")((result, operation) => {
|
|
71
|
+
const operationId = operation.operation.operationId;
|
|
72
|
+
const schema = operationId ? schemas.get(operationId) : undefined;
|
|
73
|
+
if (!schema)
|
|
74
|
+
return encodeHttpApiOperationResult(result);
|
|
75
|
+
return Schema.encodeUnknownEffect(schema)(result).pipe(Effect.flatMap(encodeHttpApiOperationResult), Effect.mapError((cause) => new Error("HTTP API result does not match its success schema", {
|
|
76
|
+
cause
|
|
77
|
+
})));
|
|
78
|
+
});
|
|
79
|
+
};
|
|
38
80
|
var GeneratedOperation = Schema.declare((value) => value instanceof Function).annotate({ identifier: "GeneratedOperation" });
|
|
39
81
|
var GeneratedOperationEffect = Schema.declare((value) => Effect.isEffect(value)).annotate({ identifier: "GeneratedOperationEffect" });
|
|
40
82
|
var makeGeneratedClient = (api, http, baseUrl) => EffectHttpApiClient.makeWith(api, {
|
|
@@ -70,9 +112,10 @@ var executeGeneratedOperation = Effect.fn("HttpApiClient.executeGeneratedOperati
|
|
|
70
112
|
class ApiClient extends Context.Service()("ApiClient") {
|
|
71
113
|
static layer = (config) => Layer.effect(this, Effect.gen(function* () {
|
|
72
114
|
const http = yield* HttpClient.HttpClient;
|
|
115
|
+
const encodeResult = makeHttpApiOperationResultEncoder(config.api);
|
|
73
116
|
const client = yield* makeGeneratedClient(config.api, http, config.baseUrl);
|
|
74
117
|
return {
|
|
75
|
-
encodeResult: config.encodeResult ?? ((result) =>
|
|
118
|
+
encodeResult: config.encodeResult ?? ((result, operation) => encodeResult(result, operation)),
|
|
76
119
|
execute: Effect.fn("ApiClient.execute")(function* (options) {
|
|
77
120
|
return yield* executeGeneratedOperation(client, options);
|
|
78
121
|
})
|
|
@@ -90,7 +133,7 @@ import {
|
|
|
90
133
|
Schema as Schema2,
|
|
91
134
|
SchemaRepresentation
|
|
92
135
|
} from "effect";
|
|
93
|
-
import { HttpApi as HttpApi2, OpenApi } from "effect/unstable/httpapi";
|
|
136
|
+
import { HttpApi as HttpApi2, OpenApi as OpenApi2 } from "effect/unstable/httpapi";
|
|
94
137
|
var JsonObjectSchema = Schema2.Record(Schema2.String, Schema2.Json).annotate({
|
|
95
138
|
identifier: "HttpJsonObject",
|
|
96
139
|
title: "HTTP JSON object",
|
|
@@ -263,7 +306,7 @@ var decodeHttpApiOperationInput = Effect2.fn("Http.decodeApiOperationInput")(fun
|
|
|
263
306
|
query: pickParameters("query")
|
|
264
307
|
};
|
|
265
308
|
});
|
|
266
|
-
var
|
|
309
|
+
var reflectedSchema2 = (schema) => Schema2.make(schema.ast);
|
|
267
310
|
var reflectedOperationInputSchemas = (api) => {
|
|
268
311
|
const schemas = new Map;
|
|
269
312
|
HttpApi2.reflect(api, {
|
|
@@ -271,17 +314,17 @@ var reflectedOperationInputSchemas = (api) => {
|
|
|
271
314
|
return;
|
|
272
315
|
},
|
|
273
316
|
onEndpoint: ({ endpoint, group }) => {
|
|
274
|
-
const operationId = Context2.getOrElse(endpoint.annotations,
|
|
317
|
+
const operationId = Context2.getOrElse(endpoint.annotations, OpenApi2.Identifier, () => group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`);
|
|
275
318
|
const fields = {};
|
|
276
319
|
if (endpoint.params)
|
|
277
|
-
fields.params =
|
|
320
|
+
fields.params = reflectedSchema2(endpoint.params);
|
|
278
321
|
if (endpoint.query) {
|
|
279
|
-
fields.query = Schema2.optionalKey(
|
|
322
|
+
fields.query = Schema2.optionalKey(reflectedSchema2(endpoint.query));
|
|
280
323
|
}
|
|
281
324
|
if (endpoint.headers) {
|
|
282
|
-
fields.headers = Schema2.optionalKey(
|
|
325
|
+
fields.headers = Schema2.optionalKey(reflectedSchema2(endpoint.headers));
|
|
283
326
|
}
|
|
284
|
-
const payloadSchemas = Array.from(endpoint.payload.values()).flatMap(({ schemas: schemas2 }) => schemas2.map(
|
|
327
|
+
const payloadSchemas = Array.from(endpoint.payload.values()).flatMap(({ schemas: schemas2 }) => schemas2.map(reflectedSchema2));
|
|
285
328
|
if (payloadSchemas.length === 1 && payloadSchemas[0]) {
|
|
286
329
|
fields.body = payloadSchemas[0];
|
|
287
330
|
} else if (payloadSchemas.length > 1) {
|
|
@@ -313,7 +356,7 @@ class HttpApiSpec extends Context2.Service()("HttpApiSpec", {
|
|
|
313
356
|
if (!HttpApi2.isHttpApi(api)) {
|
|
314
357
|
throw new Error("HttpApiSpec requires a valid HttpApi");
|
|
315
358
|
}
|
|
316
|
-
const spec =
|
|
359
|
+
const spec = OpenApi2.fromApi(api);
|
|
317
360
|
const reflectedSchemas = reflectedOperationInputSchemas(api);
|
|
318
361
|
const operations = httpApiOperations({
|
|
319
362
|
spec,
|
|
@@ -477,11 +520,17 @@ var httpApiCli = (args = process.argv.slice(2)) => Effect3.gen(function* () {
|
|
|
477
520
|
const cli = yield* HttpApiCli;
|
|
478
521
|
return yield* cli.run(args);
|
|
479
522
|
});
|
|
523
|
+
var formatHttpApiCliCause = Cause.pretty;
|
|
480
524
|
var runHttpApiCli = (layer, args = process.argv.slice(2)) => {
|
|
481
|
-
Effect3.
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
525
|
+
Effect3.runPromiseExit(httpApiCli(args).pipe(Effect3.provide(layer), Effect3.provide(httpApiCliEnvironmentLayer(args)))).then(Exit.match({
|
|
526
|
+
onSuccess: () => {
|
|
527
|
+
return;
|
|
528
|
+
},
|
|
529
|
+
onFailure: (cause) => {
|
|
530
|
+
console.error(formatHttpApiCliCause(cause));
|
|
531
|
+
process.exitCode = 1;
|
|
532
|
+
}
|
|
533
|
+
}));
|
|
485
534
|
};
|
|
486
535
|
export {
|
|
487
536
|
runHttpApiCli,
|
|
@@ -489,5 +538,6 @@ export {
|
|
|
489
538
|
makeHttpApiCliCommand,
|
|
490
539
|
httpApiCliEnvironmentLayer,
|
|
491
540
|
httpApiCli,
|
|
541
|
+
formatHttpApiCliCause,
|
|
492
542
|
HttpApiCli
|
|
493
543
|
};
|
|
@@ -12,12 +12,13 @@ export type ApiClientExecuteOptions = {
|
|
|
12
12
|
readonly operation: HttpApiOperationEntry;
|
|
13
13
|
readonly input: HttpApiOperationInput;
|
|
14
14
|
};
|
|
15
|
-
export type HttpApiOperationResultValue = null | string | number | boolean | Date | Uint8Array | ReadonlyArray<HttpApiOperationResultValue> | {
|
|
15
|
+
export type HttpApiOperationResultValue = null | undefined | string | number | boolean | Date | Uint8Array | ReadonlyArray<HttpApiOperationResultValue> | {
|
|
16
16
|
readonly [key: string]: HttpApiOperationResultValue;
|
|
17
17
|
};
|
|
18
18
|
export declare const HttpApiOperationResult: Schema.Union<readonly [Schema.Codec<HttpApiOperationResultValue, Json, never, never>, Schema.Undefined]>;
|
|
19
19
|
export type HttpApiOperationResult = typeof HttpApiOperationResult.Type;
|
|
20
20
|
export declare const encodeHttpApiOperationResult: (result: unknown) => Effect.Effect<string | number | boolean | Schema.JsonArray | Schema.JsonObject | null, Error, never>;
|
|
21
|
+
export declare const makeHttpApiOperationResultEncoder: <Id extends string, Groups extends HttpApiGroup.Constraint>(api: HttpApi.HttpApi<Id, Groups>) => (result: unknown, operation: HttpApiOperationEntry) => Effect.Effect<string | number | boolean | Schema.JsonArray | Schema.JsonObject | null, Error, never>;
|
|
21
22
|
export type ApiClientService = {
|
|
22
23
|
readonly encodeResult: (result: ErrorOptions["cause"], operation: HttpApiOperationEntry) => Effect.Effect<Json, Error>;
|
|
23
24
|
readonly execute: (options: ApiClientExecuteOptions) => Effect.Effect<ErrorOptions["cause"], Error>;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
// ../../src/lib/httpapi-client.ts
|
|
2
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
2
|
+
import { Context, Effect, Layer, Schema, SchemaTransformation } from "effect";
|
|
3
3
|
import { HttpClient } from "effect/unstable/http";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
HttpApi,
|
|
6
|
+
HttpApiClient as EffectHttpApiClient,
|
|
7
|
+
OpenApi
|
|
6
8
|
} from "effect/unstable/httpapi";
|
|
9
|
+
var UndefinedFromNull = Schema.Null.pipe(Schema.decodeTo(Schema.Undefined, SchemaTransformation.transform({
|
|
10
|
+
decode: () => {
|
|
11
|
+
return;
|
|
12
|
+
},
|
|
13
|
+
encode: () => null
|
|
14
|
+
})));
|
|
7
15
|
var HttpApiOperationResultValue = Schema.suspend(() => Schema.Union([
|
|
8
16
|
Schema.Null,
|
|
17
|
+
UndefinedFromNull,
|
|
9
18
|
Schema.String,
|
|
10
19
|
Schema.Number,
|
|
11
20
|
Schema.Boolean,
|
|
@@ -19,6 +28,37 @@ var HttpApiOperationResult = Schema.Union([
|
|
|
19
28
|
Schema.Undefined
|
|
20
29
|
]).annotate({ identifier: "HttpApiOperationResult" });
|
|
21
30
|
var encodeHttpApiOperationResult = Effect.fn("HttpApiClient.encodeOperationResult")((result) => Schema.encodeUnknownEffect(HttpApiOperationResult)(result).pipe(Effect.map((encoded) => encoded ?? null), Effect.mapError((cause) => new Error("HTTP API result is not serializable", { cause }))));
|
|
31
|
+
var reflectedSchema = (schema) => Schema.make(schema.ast);
|
|
32
|
+
var operationResultSchemas = (api) => {
|
|
33
|
+
const schemas = new Map;
|
|
34
|
+
HttpApi.reflect(api, {
|
|
35
|
+
onGroup: () => {
|
|
36
|
+
return;
|
|
37
|
+
},
|
|
38
|
+
onEndpoint: ({ endpoint, group, successes }) => {
|
|
39
|
+
const operationId = Context.getOrElse(endpoint.annotations, OpenApi.Identifier, () => group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`);
|
|
40
|
+
const operationSchemas = Array.from(successes.values()).flat().map(reflectedSchema);
|
|
41
|
+
if (operationSchemas.length === 1 && operationSchemas[0]) {
|
|
42
|
+
schemas.set(operationId, operationSchemas[0]);
|
|
43
|
+
} else if (operationSchemas.length > 1) {
|
|
44
|
+
schemas.set(operationId, Schema.Union(operationSchemas));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return schemas;
|
|
49
|
+
};
|
|
50
|
+
var makeHttpApiOperationResultEncoder = (api) => {
|
|
51
|
+
const schemas = operationResultSchemas(api);
|
|
52
|
+
return Effect.fn("HttpApiClient.encodeOperationResultWithSchema")((result, operation) => {
|
|
53
|
+
const operationId = operation.operation.operationId;
|
|
54
|
+
const schema = operationId ? schemas.get(operationId) : undefined;
|
|
55
|
+
if (!schema)
|
|
56
|
+
return encodeHttpApiOperationResult(result);
|
|
57
|
+
return Schema.encodeUnknownEffect(schema)(result).pipe(Effect.flatMap(encodeHttpApiOperationResult), Effect.mapError((cause) => new Error("HTTP API result does not match its success schema", {
|
|
58
|
+
cause
|
|
59
|
+
})));
|
|
60
|
+
});
|
|
61
|
+
};
|
|
22
62
|
var GeneratedOperation = Schema.declare((value) => value instanceof Function).annotate({ identifier: "GeneratedOperation" });
|
|
23
63
|
var GeneratedOperationEffect = Schema.declare((value) => Effect.isEffect(value)).annotate({ identifier: "GeneratedOperationEffect" });
|
|
24
64
|
var makeGeneratedClient = (api, http, baseUrl) => EffectHttpApiClient.makeWith(api, {
|
|
@@ -54,9 +94,10 @@ var executeGeneratedOperation = Effect.fn("HttpApiClient.executeGeneratedOperati
|
|
|
54
94
|
class ApiClient extends Context.Service()("ApiClient") {
|
|
55
95
|
static layer = (config) => Layer.effect(this, Effect.gen(function* () {
|
|
56
96
|
const http = yield* HttpClient.HttpClient;
|
|
97
|
+
const encodeResult = makeHttpApiOperationResultEncoder(config.api);
|
|
57
98
|
const client = yield* makeGeneratedClient(config.api, http, config.baseUrl);
|
|
58
99
|
return {
|
|
59
|
-
encodeResult: config.encodeResult ?? ((result) =>
|
|
100
|
+
encodeResult: config.encodeResult ?? ((result, operation) => encodeResult(result, operation)),
|
|
60
101
|
execute: Effect.fn("ApiClient.execute")(function* (options) {
|
|
61
102
|
return yield* executeGeneratedOperation(client, options);
|
|
62
103
|
})
|
|
@@ -64,6 +105,7 @@ class ApiClient extends Context.Service()("ApiClient") {
|
|
|
64
105
|
}));
|
|
65
106
|
}
|
|
66
107
|
export {
|
|
108
|
+
makeHttpApiOperationResultEncoder,
|
|
67
109
|
encodeHttpApiOperationResult,
|
|
68
110
|
HttpApiOperationResult,
|
|
69
111
|
ApiClient
|
package/dist/lib/httpapi-mcp.js
CHANGED
|
@@ -3,13 +3,22 @@ import { Context as Context3, Effect as Effect3, Layer as Layer3, Option as Opti
|
|
|
3
3
|
import { McpSchema, McpServer } from "effect/unstable/ai";
|
|
4
4
|
|
|
5
5
|
// ../../src/lib/httpapi-client.ts
|
|
6
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
6
|
+
import { Context, Effect, Layer, Schema, SchemaTransformation } from "effect";
|
|
7
7
|
import { HttpClient } from "effect/unstable/http";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
HttpApi,
|
|
10
|
+
HttpApiClient as EffectHttpApiClient,
|
|
11
|
+
OpenApi
|
|
10
12
|
} from "effect/unstable/httpapi";
|
|
13
|
+
var UndefinedFromNull = Schema.Null.pipe(Schema.decodeTo(Schema.Undefined, SchemaTransformation.transform({
|
|
14
|
+
decode: () => {
|
|
15
|
+
return;
|
|
16
|
+
},
|
|
17
|
+
encode: () => null
|
|
18
|
+
})));
|
|
11
19
|
var HttpApiOperationResultValue = Schema.suspend(() => Schema.Union([
|
|
12
20
|
Schema.Null,
|
|
21
|
+
UndefinedFromNull,
|
|
13
22
|
Schema.String,
|
|
14
23
|
Schema.Number,
|
|
15
24
|
Schema.Boolean,
|
|
@@ -23,6 +32,37 @@ var HttpApiOperationResult = Schema.Union([
|
|
|
23
32
|
Schema.Undefined
|
|
24
33
|
]).annotate({ identifier: "HttpApiOperationResult" });
|
|
25
34
|
var encodeHttpApiOperationResult = Effect.fn("HttpApiClient.encodeOperationResult")((result) => Schema.encodeUnknownEffect(HttpApiOperationResult)(result).pipe(Effect.map((encoded) => encoded ?? null), Effect.mapError((cause) => new Error("HTTP API result is not serializable", { cause }))));
|
|
35
|
+
var reflectedSchema = (schema) => Schema.make(schema.ast);
|
|
36
|
+
var operationResultSchemas = (api) => {
|
|
37
|
+
const schemas = new Map;
|
|
38
|
+
HttpApi.reflect(api, {
|
|
39
|
+
onGroup: () => {
|
|
40
|
+
return;
|
|
41
|
+
},
|
|
42
|
+
onEndpoint: ({ endpoint, group, successes }) => {
|
|
43
|
+
const operationId = Context.getOrElse(endpoint.annotations, OpenApi.Identifier, () => group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`);
|
|
44
|
+
const operationSchemas = Array.from(successes.values()).flat().map(reflectedSchema);
|
|
45
|
+
if (operationSchemas.length === 1 && operationSchemas[0]) {
|
|
46
|
+
schemas.set(operationId, operationSchemas[0]);
|
|
47
|
+
} else if (operationSchemas.length > 1) {
|
|
48
|
+
schemas.set(operationId, Schema.Union(operationSchemas));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return schemas;
|
|
53
|
+
};
|
|
54
|
+
var makeHttpApiOperationResultEncoder = (api) => {
|
|
55
|
+
const schemas = operationResultSchemas(api);
|
|
56
|
+
return Effect.fn("HttpApiClient.encodeOperationResultWithSchema")((result, operation) => {
|
|
57
|
+
const operationId = operation.operation.operationId;
|
|
58
|
+
const schema = operationId ? schemas.get(operationId) : undefined;
|
|
59
|
+
if (!schema)
|
|
60
|
+
return encodeHttpApiOperationResult(result);
|
|
61
|
+
return Schema.encodeUnknownEffect(schema)(result).pipe(Effect.flatMap(encodeHttpApiOperationResult), Effect.mapError((cause) => new Error("HTTP API result does not match its success schema", {
|
|
62
|
+
cause
|
|
63
|
+
})));
|
|
64
|
+
});
|
|
65
|
+
};
|
|
26
66
|
var GeneratedOperation = Schema.declare((value) => value instanceof Function).annotate({ identifier: "GeneratedOperation" });
|
|
27
67
|
var GeneratedOperationEffect = Schema.declare((value) => Effect.isEffect(value)).annotate({ identifier: "GeneratedOperationEffect" });
|
|
28
68
|
var makeGeneratedClient = (api, http, baseUrl) => EffectHttpApiClient.makeWith(api, {
|
|
@@ -58,9 +98,10 @@ var executeGeneratedOperation = Effect.fn("HttpApiClient.executeGeneratedOperati
|
|
|
58
98
|
class ApiClient extends Context.Service()("ApiClient") {
|
|
59
99
|
static layer = (config) => Layer.effect(this, Effect.gen(function* () {
|
|
60
100
|
const http = yield* HttpClient.HttpClient;
|
|
101
|
+
const encodeResult = makeHttpApiOperationResultEncoder(config.api);
|
|
61
102
|
const client = yield* makeGeneratedClient(config.api, http, config.baseUrl);
|
|
62
103
|
return {
|
|
63
|
-
encodeResult: config.encodeResult ?? ((result) =>
|
|
104
|
+
encodeResult: config.encodeResult ?? ((result, operation) => encodeResult(result, operation)),
|
|
64
105
|
execute: Effect.fn("ApiClient.execute")(function* (options) {
|
|
65
106
|
return yield* executeGeneratedOperation(client, options);
|
|
66
107
|
})
|
|
@@ -78,7 +119,7 @@ import {
|
|
|
78
119
|
Schema as Schema2,
|
|
79
120
|
SchemaRepresentation
|
|
80
121
|
} from "effect";
|
|
81
|
-
import { HttpApi as HttpApi2, OpenApi } from "effect/unstable/httpapi";
|
|
122
|
+
import { HttpApi as HttpApi2, OpenApi as OpenApi2 } from "effect/unstable/httpapi";
|
|
82
123
|
var JsonObjectSchema = Schema2.Record(Schema2.String, Schema2.Json).annotate({
|
|
83
124
|
identifier: "HttpJsonObject",
|
|
84
125
|
title: "HTTP JSON object",
|
|
@@ -251,7 +292,7 @@ var decodeHttpApiOperationInput = Effect2.fn("Http.decodeApiOperationInput")(fun
|
|
|
251
292
|
query: pickParameters("query")
|
|
252
293
|
};
|
|
253
294
|
});
|
|
254
|
-
var
|
|
295
|
+
var reflectedSchema2 = (schema) => Schema2.make(schema.ast);
|
|
255
296
|
var reflectedOperationInputSchemas = (api) => {
|
|
256
297
|
const schemas = new Map;
|
|
257
298
|
HttpApi2.reflect(api, {
|
|
@@ -259,17 +300,17 @@ var reflectedOperationInputSchemas = (api) => {
|
|
|
259
300
|
return;
|
|
260
301
|
},
|
|
261
302
|
onEndpoint: ({ endpoint, group }) => {
|
|
262
|
-
const operationId = Context2.getOrElse(endpoint.annotations,
|
|
303
|
+
const operationId = Context2.getOrElse(endpoint.annotations, OpenApi2.Identifier, () => group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`);
|
|
263
304
|
const fields = {};
|
|
264
305
|
if (endpoint.params)
|
|
265
|
-
fields.params =
|
|
306
|
+
fields.params = reflectedSchema2(endpoint.params);
|
|
266
307
|
if (endpoint.query) {
|
|
267
|
-
fields.query = Schema2.optionalKey(
|
|
308
|
+
fields.query = Schema2.optionalKey(reflectedSchema2(endpoint.query));
|
|
268
309
|
}
|
|
269
310
|
if (endpoint.headers) {
|
|
270
|
-
fields.headers = Schema2.optionalKey(
|
|
311
|
+
fields.headers = Schema2.optionalKey(reflectedSchema2(endpoint.headers));
|
|
271
312
|
}
|
|
272
|
-
const payloadSchemas = Array.from(endpoint.payload.values()).flatMap(({ schemas: schemas2 }) => schemas2.map(
|
|
313
|
+
const payloadSchemas = Array.from(endpoint.payload.values()).flatMap(({ schemas: schemas2 }) => schemas2.map(reflectedSchema2));
|
|
273
314
|
if (payloadSchemas.length === 1 && payloadSchemas[0]) {
|
|
274
315
|
fields.body = payloadSchemas[0];
|
|
275
316
|
} else if (payloadSchemas.length > 1) {
|
|
@@ -301,7 +342,7 @@ class HttpApiSpec extends Context2.Service()("HttpApiSpec", {
|
|
|
301
342
|
if (!HttpApi2.isHttpApi(api)) {
|
|
302
343
|
throw new Error("HttpApiSpec requires a valid HttpApi");
|
|
303
344
|
}
|
|
304
|
-
const spec =
|
|
345
|
+
const spec = OpenApi2.fromApi(api);
|
|
305
346
|
const reflectedSchemas = reflectedOperationInputSchemas(api);
|
|
306
347
|
const operations = httpApiOperations({
|
|
307
348
|
spec,
|
|
@@ -3,13 +3,22 @@ import { Effect as Effect3, Layer as Layer3, Schema as Schema3 } from "effect";
|
|
|
3
3
|
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
4
4
|
|
|
5
5
|
// ../../src/lib/httpapi-client.ts
|
|
6
|
-
import { Context, Effect, Layer, Schema } from "effect";
|
|
6
|
+
import { Context, Effect, Layer, Schema, SchemaTransformation } from "effect";
|
|
7
7
|
import { HttpClient } from "effect/unstable/http";
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
HttpApi,
|
|
10
|
+
HttpApiClient as EffectHttpApiClient,
|
|
11
|
+
OpenApi
|
|
10
12
|
} from "effect/unstable/httpapi";
|
|
13
|
+
var UndefinedFromNull = Schema.Null.pipe(Schema.decodeTo(Schema.Undefined, SchemaTransformation.transform({
|
|
14
|
+
decode: () => {
|
|
15
|
+
return;
|
|
16
|
+
},
|
|
17
|
+
encode: () => null
|
|
18
|
+
})));
|
|
11
19
|
var HttpApiOperationResultValue = Schema.suspend(() => Schema.Union([
|
|
12
20
|
Schema.Null,
|
|
21
|
+
UndefinedFromNull,
|
|
13
22
|
Schema.String,
|
|
14
23
|
Schema.Number,
|
|
15
24
|
Schema.Boolean,
|
|
@@ -23,6 +32,37 @@ var HttpApiOperationResult = Schema.Union([
|
|
|
23
32
|
Schema.Undefined
|
|
24
33
|
]).annotate({ identifier: "HttpApiOperationResult" });
|
|
25
34
|
var encodeHttpApiOperationResult = Effect.fn("HttpApiClient.encodeOperationResult")((result) => Schema.encodeUnknownEffect(HttpApiOperationResult)(result).pipe(Effect.map((encoded) => encoded ?? null), Effect.mapError((cause) => new Error("HTTP API result is not serializable", { cause }))));
|
|
35
|
+
var reflectedSchema = (schema) => Schema.make(schema.ast);
|
|
36
|
+
var operationResultSchemas = (api) => {
|
|
37
|
+
const schemas = new Map;
|
|
38
|
+
HttpApi.reflect(api, {
|
|
39
|
+
onGroup: () => {
|
|
40
|
+
return;
|
|
41
|
+
},
|
|
42
|
+
onEndpoint: ({ endpoint, group, successes }) => {
|
|
43
|
+
const operationId = Context.getOrElse(endpoint.annotations, OpenApi.Identifier, () => group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`);
|
|
44
|
+
const operationSchemas = Array.from(successes.values()).flat().map(reflectedSchema);
|
|
45
|
+
if (operationSchemas.length === 1 && operationSchemas[0]) {
|
|
46
|
+
schemas.set(operationId, operationSchemas[0]);
|
|
47
|
+
} else if (operationSchemas.length > 1) {
|
|
48
|
+
schemas.set(operationId, Schema.Union(operationSchemas));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return schemas;
|
|
53
|
+
};
|
|
54
|
+
var makeHttpApiOperationResultEncoder = (api) => {
|
|
55
|
+
const schemas = operationResultSchemas(api);
|
|
56
|
+
return Effect.fn("HttpApiClient.encodeOperationResultWithSchema")((result, operation) => {
|
|
57
|
+
const operationId = operation.operation.operationId;
|
|
58
|
+
const schema = operationId ? schemas.get(operationId) : undefined;
|
|
59
|
+
if (!schema)
|
|
60
|
+
return encodeHttpApiOperationResult(result);
|
|
61
|
+
return Schema.encodeUnknownEffect(schema)(result).pipe(Effect.flatMap(encodeHttpApiOperationResult), Effect.mapError((cause) => new Error("HTTP API result does not match its success schema", {
|
|
62
|
+
cause
|
|
63
|
+
})));
|
|
64
|
+
});
|
|
65
|
+
};
|
|
26
66
|
var GeneratedOperation = Schema.declare((value) => value instanceof Function).annotate({ identifier: "GeneratedOperation" });
|
|
27
67
|
var GeneratedOperationEffect = Schema.declare((value) => Effect.isEffect(value)).annotate({ identifier: "GeneratedOperationEffect" });
|
|
28
68
|
var makeGeneratedClient = (api, http, baseUrl) => EffectHttpApiClient.makeWith(api, {
|
|
@@ -58,9 +98,10 @@ var executeGeneratedOperation = Effect.fn("HttpApiClient.executeGeneratedOperati
|
|
|
58
98
|
class ApiClient extends Context.Service()("ApiClient") {
|
|
59
99
|
static layer = (config) => Layer.effect(this, Effect.gen(function* () {
|
|
60
100
|
const http = yield* HttpClient.HttpClient;
|
|
101
|
+
const encodeResult = makeHttpApiOperationResultEncoder(config.api);
|
|
61
102
|
const client = yield* makeGeneratedClient(config.api, http, config.baseUrl);
|
|
62
103
|
return {
|
|
63
|
-
encodeResult: config.encodeResult ?? ((result) =>
|
|
104
|
+
encodeResult: config.encodeResult ?? ((result, operation) => encodeResult(result, operation)),
|
|
64
105
|
execute: Effect.fn("ApiClient.execute")(function* (options) {
|
|
65
106
|
return yield* executeGeneratedOperation(client, options);
|
|
66
107
|
})
|
|
@@ -78,7 +119,7 @@ import {
|
|
|
78
119
|
Schema as Schema2,
|
|
79
120
|
SchemaRepresentation
|
|
80
121
|
} from "effect";
|
|
81
|
-
import { HttpApi as HttpApi2, OpenApi } from "effect/unstable/httpapi";
|
|
122
|
+
import { HttpApi as HttpApi2, OpenApi as OpenApi2 } from "effect/unstable/httpapi";
|
|
82
123
|
var JsonObjectSchema = Schema2.Record(Schema2.String, Schema2.Json).annotate({
|
|
83
124
|
identifier: "HttpJsonObject",
|
|
84
125
|
title: "HTTP JSON object",
|
|
@@ -251,7 +292,7 @@ var decodeHttpApiOperationInput = Effect2.fn("Http.decodeApiOperationInput")(fun
|
|
|
251
292
|
query: pickParameters("query")
|
|
252
293
|
};
|
|
253
294
|
});
|
|
254
|
-
var
|
|
295
|
+
var reflectedSchema2 = (schema) => Schema2.make(schema.ast);
|
|
255
296
|
var reflectedOperationInputSchemas = (api) => {
|
|
256
297
|
const schemas = new Map;
|
|
257
298
|
HttpApi2.reflect(api, {
|
|
@@ -259,17 +300,17 @@ var reflectedOperationInputSchemas = (api) => {
|
|
|
259
300
|
return;
|
|
260
301
|
},
|
|
261
302
|
onEndpoint: ({ endpoint, group }) => {
|
|
262
|
-
const operationId = Context2.getOrElse(endpoint.annotations,
|
|
303
|
+
const operationId = Context2.getOrElse(endpoint.annotations, OpenApi2.Identifier, () => group.topLevel ? endpoint.identifier : `${group.identifier}.${endpoint.identifier}`);
|
|
263
304
|
const fields = {};
|
|
264
305
|
if (endpoint.params)
|
|
265
|
-
fields.params =
|
|
306
|
+
fields.params = reflectedSchema2(endpoint.params);
|
|
266
307
|
if (endpoint.query) {
|
|
267
|
-
fields.query = Schema2.optionalKey(
|
|
308
|
+
fields.query = Schema2.optionalKey(reflectedSchema2(endpoint.query));
|
|
268
309
|
}
|
|
269
310
|
if (endpoint.headers) {
|
|
270
|
-
fields.headers = Schema2.optionalKey(
|
|
311
|
+
fields.headers = Schema2.optionalKey(reflectedSchema2(endpoint.headers));
|
|
271
312
|
}
|
|
272
|
-
const payloadSchemas = Array.from(endpoint.payload.values()).flatMap(({ schemas: schemas2 }) => schemas2.map(
|
|
313
|
+
const payloadSchemas = Array.from(endpoint.payload.values()).flatMap(({ schemas: schemas2 }) => schemas2.map(reflectedSchema2));
|
|
273
314
|
if (payloadSchemas.length === 1 && payloadSchemas[0]) {
|
|
274
315
|
fields.body = payloadSchemas[0];
|
|
275
316
|
} else if (payloadSchemas.length > 1) {
|
|
@@ -301,7 +342,7 @@ class HttpApiSpec extends Context2.Service()("HttpApiSpec", {
|
|
|
301
342
|
if (!HttpApi2.isHttpApi(api)) {
|
|
302
343
|
throw new Error("HttpApiSpec requires a valid HttpApi");
|
|
303
344
|
}
|
|
304
|
-
const spec =
|
|
345
|
+
const spec = OpenApi2.fromApi(api);
|
|
305
346
|
const reflectedSchemas = reflectedOperationInputSchemas(api);
|
|
306
347
|
const operations = httpApiOperations({
|
|
307
348
|
spec,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { HttpRouter } from "effect/unstable/http";
|
|
2
|
+
import { HttpApiBuilder, HttpApiGroup } from "effect/unstable/httpapi";
|
|
3
|
+
import { HealthApiGroup } from "./api.group.js";
|
|
4
|
+
import { HealthService } from "./index.js";
|
|
5
|
+
export declare const healthHandler: <const Prefix extends HttpRouter.PathInput>(handlers: HttpApiBuilder.Handlers.FromGroup<HttpApiGroup.AddPrefix<typeof HealthApiGroup, Prefix>>) => HttpApiBuilder.Handlers<HttpRouter.Request<"Requires", HealthService>, {
|
|
6
|
+
readonly getHealth: import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"getHealth", "GET", `${Prefix}/health`, never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
7
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
8
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
9
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
10
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
11
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
12
|
+
}>>;
|
|
13
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
14
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
15
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
16
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
17
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
18
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
19
|
+
}>>;
|
|
20
|
+
}> | typeof import("effect/unstable/httpapi/HttpApiError").InternalServerError>, never, never>;
|
|
21
|
+
readonly getLiveness: import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"getLiveness", "GET", `${Prefix}/health/live`, never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
22
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
23
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
24
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
25
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
26
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
27
|
+
}>>;
|
|
28
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
29
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
30
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
31
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
32
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
33
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
34
|
+
}>>;
|
|
35
|
+
}> | typeof import("effect/unstable/httpapi/HttpApiError").InternalServerError>, never, never>;
|
|
36
|
+
readonly getReadiness: import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"getReadiness", "GET", `${Prefix}/health/ready`, never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
37
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
38
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
39
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
40
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
41
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
42
|
+
}>>;
|
|
43
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
44
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
45
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
46
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
47
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
48
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
49
|
+
}>>;
|
|
50
|
+
}> | typeof import("effect/unstable/httpapi/HttpApiError").InternalServerError>, never, never>;
|
|
51
|
+
readonly getStartup: import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"getStartup", "GET", `${Prefix}/health/started`, never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
52
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
53
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
54
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
55
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
56
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
57
|
+
}>>;
|
|
58
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
59
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
60
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
61
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
62
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
63
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
64
|
+
}>>;
|
|
65
|
+
}> | typeof import("effect/unstable/httpapi/HttpApiError").InternalServerError>, never, never>;
|
|
66
|
+
}, "getHealth" | "getLiveness" | "getReadiness" | "getStartup">;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { HttpApiEndpoint, HttpApiError, HttpApiGroup } from "effect/unstable/httpapi";
|
|
2
|
+
export declare const HealthApiGroup: HttpApiGroup.HttpApiGroup<"health", HttpApiEndpoint.HttpApiEndpoint<"getHealth", "GET", "/health", never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
3
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
4
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
5
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
6
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
7
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
8
|
+
}>>;
|
|
9
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
10
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
11
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
12
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
13
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
14
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
15
|
+
}>>;
|
|
16
|
+
}> | typeof HttpApiError.InternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getLiveness", "GET", "/health/live", never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
17
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
18
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
19
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
20
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
21
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
22
|
+
}>>;
|
|
23
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
24
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
25
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
26
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
27
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
28
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
29
|
+
}>>;
|
|
30
|
+
}> | typeof HttpApiError.InternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getReadiness", "GET", "/health/ready", never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
31
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
32
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
33
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
34
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
35
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
36
|
+
}>>;
|
|
37
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
38
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
39
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
40
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
41
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
42
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
43
|
+
}>>;
|
|
44
|
+
}> | typeof HttpApiError.InternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getStartup", "GET", "/health/started", never, never, never, never, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
45
|
+
readonly status: import("effect/Schema").Literal<"UP">;
|
|
46
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
47
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
48
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
49
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
50
|
+
}>>;
|
|
51
|
+
}>>, import("effect/Schema").toCodecJson<import("effect/Schema").Struct<{
|
|
52
|
+
readonly status: import("effect/Schema").Literal<"DOWN">;
|
|
53
|
+
readonly checks: import("effect/Schema").$Array<import("effect/Schema").Struct<{
|
|
54
|
+
readonly name: import("effect/Schema").NonEmptyString;
|
|
55
|
+
readonly status: import("effect/Schema").Literals<readonly ["UP", "DOWN"]>;
|
|
56
|
+
readonly data: import("effect/Schema").optionalKey<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Union<readonly [import("effect/Schema").String, import("effect/Schema").Boolean, import("effect/Schema").Number]>>>;
|
|
57
|
+
}>>;
|
|
58
|
+
}> | typeof HttpApiError.InternalServerError>, never, never>, false>;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
import type { HealthCheckData, HealthCheckOutcome } from "./schema.js";
|
|
3
|
+
export type HealthCheck<Requirements = never> = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly check: Effect.Effect<HealthCheckOutcome, unknown, Requirements>;
|
|
6
|
+
};
|
|
7
|
+
export type HealthServiceChecks<Requirements = never> = {
|
|
8
|
+
readonly live?: ReadonlyArray<HealthCheck<Requirements>>;
|
|
9
|
+
readonly ready?: ReadonlyArray<HealthCheck<Requirements>>;
|
|
10
|
+
readonly started?: ReadonlyArray<HealthCheck<Requirements>>;
|
|
11
|
+
};
|
|
12
|
+
export type HealthServiceOptions<Requirements = never> = {
|
|
13
|
+
readonly checks?: HealthServiceChecks<Requirements>;
|
|
14
|
+
};
|
|
15
|
+
type RegisteredHealthChecks = {
|
|
16
|
+
readonly live: ReadonlyArray<HealthCheck>;
|
|
17
|
+
readonly ready: ReadonlyArray<HealthCheck>;
|
|
18
|
+
readonly started: ReadonlyArray<HealthCheck>;
|
|
19
|
+
};
|
|
20
|
+
declare const HealthServiceConfig_base: Context.ServiceClass<HealthServiceConfig, "@krak-stack/registry/HealthServiceConfig", RegisteredHealthChecks>;
|
|
21
|
+
declare class HealthServiceConfig extends HealthServiceConfig_base {
|
|
22
|
+
static readonly layerWith: <Requirements = never>({ checks, }?: HealthServiceOptions<Requirements>) => Layer.Layer<HealthServiceConfig, never, Exclude<Requirements, import("effect/Scope").Scope>>;
|
|
23
|
+
}
|
|
24
|
+
declare const HealthService_base: Context.ServiceClass<HealthService, "@krak-stack/registry/HealthService", {
|
|
25
|
+
aggregate: () => Effect.Effect<{
|
|
26
|
+
status: "UP";
|
|
27
|
+
checks: ({
|
|
28
|
+
name: string;
|
|
29
|
+
status: "DOWN" | "UP";
|
|
30
|
+
data?: undefined;
|
|
31
|
+
} | {
|
|
32
|
+
name: string;
|
|
33
|
+
status: "DOWN" | "UP";
|
|
34
|
+
data: {
|
|
35
|
+
readonly [x: string]: string | number | boolean;
|
|
36
|
+
};
|
|
37
|
+
})[];
|
|
38
|
+
} | {
|
|
39
|
+
status: "DOWN";
|
|
40
|
+
checks: ({
|
|
41
|
+
name: string;
|
|
42
|
+
status: "DOWN" | "UP";
|
|
43
|
+
data?: undefined;
|
|
44
|
+
} | {
|
|
45
|
+
name: string;
|
|
46
|
+
status: "DOWN" | "UP";
|
|
47
|
+
data: {
|
|
48
|
+
readonly [x: string]: string | number | boolean;
|
|
49
|
+
};
|
|
50
|
+
})[];
|
|
51
|
+
}, never, never>;
|
|
52
|
+
live: () => Effect.Effect<{
|
|
53
|
+
status: "UP";
|
|
54
|
+
checks: ({
|
|
55
|
+
name: string;
|
|
56
|
+
status: "DOWN" | "UP";
|
|
57
|
+
data?: undefined;
|
|
58
|
+
} | {
|
|
59
|
+
name: string;
|
|
60
|
+
status: "DOWN" | "UP";
|
|
61
|
+
data: {
|
|
62
|
+
readonly [x: string]: string | number | boolean;
|
|
63
|
+
};
|
|
64
|
+
})[];
|
|
65
|
+
} | {
|
|
66
|
+
status: "DOWN";
|
|
67
|
+
checks: ({
|
|
68
|
+
name: string;
|
|
69
|
+
status: "DOWN" | "UP";
|
|
70
|
+
data?: undefined;
|
|
71
|
+
} | {
|
|
72
|
+
name: string;
|
|
73
|
+
status: "DOWN" | "UP";
|
|
74
|
+
data: {
|
|
75
|
+
readonly [x: string]: string | number | boolean;
|
|
76
|
+
};
|
|
77
|
+
})[];
|
|
78
|
+
}, never, never>;
|
|
79
|
+
ready: () => Effect.Effect<{
|
|
80
|
+
status: "UP";
|
|
81
|
+
checks: ({
|
|
82
|
+
name: string;
|
|
83
|
+
status: "DOWN" | "UP";
|
|
84
|
+
data?: undefined;
|
|
85
|
+
} | {
|
|
86
|
+
name: string;
|
|
87
|
+
status: "DOWN" | "UP";
|
|
88
|
+
data: {
|
|
89
|
+
readonly [x: string]: string | number | boolean;
|
|
90
|
+
};
|
|
91
|
+
})[];
|
|
92
|
+
} | {
|
|
93
|
+
status: "DOWN";
|
|
94
|
+
checks: ({
|
|
95
|
+
name: string;
|
|
96
|
+
status: "DOWN" | "UP";
|
|
97
|
+
data?: undefined;
|
|
98
|
+
} | {
|
|
99
|
+
name: string;
|
|
100
|
+
status: "DOWN" | "UP";
|
|
101
|
+
data: {
|
|
102
|
+
readonly [x: string]: string | number | boolean;
|
|
103
|
+
};
|
|
104
|
+
})[];
|
|
105
|
+
}, never, never>;
|
|
106
|
+
started: () => Effect.Effect<{
|
|
107
|
+
status: "UP";
|
|
108
|
+
checks: ({
|
|
109
|
+
name: string;
|
|
110
|
+
status: "DOWN" | "UP";
|
|
111
|
+
data?: undefined;
|
|
112
|
+
} | {
|
|
113
|
+
name: string;
|
|
114
|
+
status: "DOWN" | "UP";
|
|
115
|
+
data: {
|
|
116
|
+
readonly [x: string]: string | number | boolean;
|
|
117
|
+
};
|
|
118
|
+
})[];
|
|
119
|
+
} | {
|
|
120
|
+
status: "DOWN";
|
|
121
|
+
checks: ({
|
|
122
|
+
name: string;
|
|
123
|
+
status: "DOWN" | "UP";
|
|
124
|
+
data?: undefined;
|
|
125
|
+
} | {
|
|
126
|
+
name: string;
|
|
127
|
+
status: "DOWN" | "UP";
|
|
128
|
+
data: {
|
|
129
|
+
readonly [x: string]: string | number | boolean;
|
|
130
|
+
};
|
|
131
|
+
})[];
|
|
132
|
+
}, never, never>;
|
|
133
|
+
}> & {
|
|
134
|
+
readonly make: Effect.Effect<{
|
|
135
|
+
aggregate: () => Effect.Effect<{
|
|
136
|
+
status: "UP";
|
|
137
|
+
checks: ({
|
|
138
|
+
name: string;
|
|
139
|
+
status: "DOWN" | "UP";
|
|
140
|
+
data?: undefined;
|
|
141
|
+
} | {
|
|
142
|
+
name: string;
|
|
143
|
+
status: "DOWN" | "UP";
|
|
144
|
+
data: {
|
|
145
|
+
readonly [x: string]: string | number | boolean;
|
|
146
|
+
};
|
|
147
|
+
})[];
|
|
148
|
+
} | {
|
|
149
|
+
status: "DOWN";
|
|
150
|
+
checks: ({
|
|
151
|
+
name: string;
|
|
152
|
+
status: "DOWN" | "UP";
|
|
153
|
+
data?: undefined;
|
|
154
|
+
} | {
|
|
155
|
+
name: string;
|
|
156
|
+
status: "DOWN" | "UP";
|
|
157
|
+
data: {
|
|
158
|
+
readonly [x: string]: string | number | boolean;
|
|
159
|
+
};
|
|
160
|
+
})[];
|
|
161
|
+
}, never, never>;
|
|
162
|
+
live: () => Effect.Effect<{
|
|
163
|
+
status: "UP";
|
|
164
|
+
checks: ({
|
|
165
|
+
name: string;
|
|
166
|
+
status: "DOWN" | "UP";
|
|
167
|
+
data?: undefined;
|
|
168
|
+
} | {
|
|
169
|
+
name: string;
|
|
170
|
+
status: "DOWN" | "UP";
|
|
171
|
+
data: {
|
|
172
|
+
readonly [x: string]: string | number | boolean;
|
|
173
|
+
};
|
|
174
|
+
})[];
|
|
175
|
+
} | {
|
|
176
|
+
status: "DOWN";
|
|
177
|
+
checks: ({
|
|
178
|
+
name: string;
|
|
179
|
+
status: "DOWN" | "UP";
|
|
180
|
+
data?: undefined;
|
|
181
|
+
} | {
|
|
182
|
+
name: string;
|
|
183
|
+
status: "DOWN" | "UP";
|
|
184
|
+
data: {
|
|
185
|
+
readonly [x: string]: string | number | boolean;
|
|
186
|
+
};
|
|
187
|
+
})[];
|
|
188
|
+
}, never, never>;
|
|
189
|
+
ready: () => Effect.Effect<{
|
|
190
|
+
status: "UP";
|
|
191
|
+
checks: ({
|
|
192
|
+
name: string;
|
|
193
|
+
status: "DOWN" | "UP";
|
|
194
|
+
data?: undefined;
|
|
195
|
+
} | {
|
|
196
|
+
name: string;
|
|
197
|
+
status: "DOWN" | "UP";
|
|
198
|
+
data: {
|
|
199
|
+
readonly [x: string]: string | number | boolean;
|
|
200
|
+
};
|
|
201
|
+
})[];
|
|
202
|
+
} | {
|
|
203
|
+
status: "DOWN";
|
|
204
|
+
checks: ({
|
|
205
|
+
name: string;
|
|
206
|
+
status: "DOWN" | "UP";
|
|
207
|
+
data?: undefined;
|
|
208
|
+
} | {
|
|
209
|
+
name: string;
|
|
210
|
+
status: "DOWN" | "UP";
|
|
211
|
+
data: {
|
|
212
|
+
readonly [x: string]: string | number | boolean;
|
|
213
|
+
};
|
|
214
|
+
})[];
|
|
215
|
+
}, never, never>;
|
|
216
|
+
started: () => Effect.Effect<{
|
|
217
|
+
status: "UP";
|
|
218
|
+
checks: ({
|
|
219
|
+
name: string;
|
|
220
|
+
status: "DOWN" | "UP";
|
|
221
|
+
data?: undefined;
|
|
222
|
+
} | {
|
|
223
|
+
name: string;
|
|
224
|
+
status: "DOWN" | "UP";
|
|
225
|
+
data: {
|
|
226
|
+
readonly [x: string]: string | number | boolean;
|
|
227
|
+
};
|
|
228
|
+
})[];
|
|
229
|
+
} | {
|
|
230
|
+
status: "DOWN";
|
|
231
|
+
checks: ({
|
|
232
|
+
name: string;
|
|
233
|
+
status: "DOWN" | "UP";
|
|
234
|
+
data?: undefined;
|
|
235
|
+
} | {
|
|
236
|
+
name: string;
|
|
237
|
+
status: "DOWN" | "UP";
|
|
238
|
+
data: {
|
|
239
|
+
readonly [x: string]: string | number | boolean;
|
|
240
|
+
};
|
|
241
|
+
})[];
|
|
242
|
+
}, never, never>;
|
|
243
|
+
}, never, HealthServiceConfig>;
|
|
244
|
+
};
|
|
245
|
+
export declare class HealthService extends HealthService_base {
|
|
246
|
+
static readonly up: (data?: HealthCheckData) => HealthCheckOutcome;
|
|
247
|
+
static readonly down: (data?: HealthCheckData) => HealthCheckOutcome;
|
|
248
|
+
static readonly layer: Layer.Layer<HealthService, never, never>;
|
|
249
|
+
static readonly layerWith: <Requirements = never>(options: HealthServiceOptions<Requirements>) => Layer.Layer<HealthService, never, Exclude<Requirements, import("effect/Scope").Scope>>;
|
|
250
|
+
}
|
|
251
|
+
export * from "./api.builder.js";
|
|
252
|
+
export * from "./api.group.js";
|
|
253
|
+
export * from "./schema.js";
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// ../../src/services/health/index.ts
|
|
2
|
+
import { Context, Effect as Effect2, Exit, Layer } from "effect";
|
|
3
|
+
|
|
4
|
+
// ../../src/services/health/api.builder.ts
|
|
5
|
+
import { Effect } from "effect";
|
|
6
|
+
var respond = (check) => check.pipe(Effect.flatMap((response) => response.status === "UP" ? Effect.succeed(response) : Effect.fail(response)));
|
|
7
|
+
var healthHandler = (handlers) => handlers.handle("getHealth", () => Effect.flatMap(HealthService, ({ aggregate }) => respond(aggregate()))).handle("getLiveness", () => Effect.flatMap(HealthService, ({ live }) => respond(live()))).handle("getReadiness", () => Effect.flatMap(HealthService, ({ ready }) => respond(ready()))).handle("getStartup", () => Effect.flatMap(HealthService, ({ started }) => respond(started())));
|
|
8
|
+
// ../../src/services/health/api.group.ts
|
|
9
|
+
import {
|
|
10
|
+
HttpApiEndpoint,
|
|
11
|
+
HttpApiError,
|
|
12
|
+
HttpApiGroup,
|
|
13
|
+
OpenApi
|
|
14
|
+
} from "effect/unstable/httpapi";
|
|
15
|
+
|
|
16
|
+
// ../../src/services/health/schema.ts
|
|
17
|
+
import { Schema } from "effect";
|
|
18
|
+
import { HttpApiSchema } from "effect/unstable/httpapi";
|
|
19
|
+
var HealthStatus = Schema.Literals(["UP", "DOWN"]).annotate({
|
|
20
|
+
identifier: "HealthStatus",
|
|
21
|
+
title: "Health Status",
|
|
22
|
+
description: "Whether a health check is up or down"
|
|
23
|
+
});
|
|
24
|
+
var HealthCheckData = Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Boolean, Schema.Number])).annotate({
|
|
25
|
+
identifier: "HealthCheckData",
|
|
26
|
+
title: "Health Check Data",
|
|
27
|
+
description: "Non-sensitive diagnostic values for a health check"
|
|
28
|
+
});
|
|
29
|
+
var HealthCheckOutcome = Schema.Struct({
|
|
30
|
+
status: HealthStatus,
|
|
31
|
+
data: Schema.optionalKey(HealthCheckData)
|
|
32
|
+
}).annotate({
|
|
33
|
+
identifier: "HealthCheckOutcome",
|
|
34
|
+
title: "Health Check Outcome",
|
|
35
|
+
description: "The status and optional diagnostic data returned by a check"
|
|
36
|
+
});
|
|
37
|
+
var HealthCheckResult = Schema.Struct({
|
|
38
|
+
name: Schema.NonEmptyString,
|
|
39
|
+
status: HealthStatus,
|
|
40
|
+
data: Schema.optionalKey(HealthCheckData)
|
|
41
|
+
}).annotate({
|
|
42
|
+
identifier: "HealthCheckResult",
|
|
43
|
+
title: "Health Check Result",
|
|
44
|
+
description: "The outcome of a named health check"
|
|
45
|
+
});
|
|
46
|
+
var HealthUpResponse = Schema.Struct({
|
|
47
|
+
status: Schema.Literal("UP"),
|
|
48
|
+
checks: Schema.Array(HealthCheckResult)
|
|
49
|
+
}).annotate({
|
|
50
|
+
identifier: "HealthUpResponse",
|
|
51
|
+
title: "Healthy Response",
|
|
52
|
+
description: "A response where all health checks are up",
|
|
53
|
+
examples: [{ status: "UP", checks: [] }]
|
|
54
|
+
});
|
|
55
|
+
var HealthDownResponse = Schema.Struct({
|
|
56
|
+
status: Schema.Literal("DOWN"),
|
|
57
|
+
checks: Schema.Array(HealthCheckResult)
|
|
58
|
+
}).pipe(HttpApiSchema.status(503)).annotate({
|
|
59
|
+
identifier: "HealthDownResponse",
|
|
60
|
+
title: "Unhealthy Response",
|
|
61
|
+
description: "A response where one or more health checks are down",
|
|
62
|
+
examples: [
|
|
63
|
+
{
|
|
64
|
+
status: "DOWN",
|
|
65
|
+
checks: [{ name: "database", status: "DOWN" }]
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
var HealthResponse = Schema.Union([
|
|
70
|
+
HealthUpResponse,
|
|
71
|
+
HealthDownResponse
|
|
72
|
+
]).annotate({
|
|
73
|
+
identifier: "HealthResponse",
|
|
74
|
+
title: "Health Response",
|
|
75
|
+
description: "An aggregate health response"
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ../../src/services/health/api.group.ts
|
|
79
|
+
var errors = [HealthDownResponse, HttpApiError.InternalServerError];
|
|
80
|
+
var HealthApiGroup = HttpApiGroup.make("health").annotateMerge(OpenApi.annotations({
|
|
81
|
+
title: "Health",
|
|
82
|
+
description: "Application health checks"
|
|
83
|
+
})).add(HttpApiEndpoint.get("getHealth", "/health", {
|
|
84
|
+
success: HealthUpResponse,
|
|
85
|
+
error: errors
|
|
86
|
+
}).annotateMerge(OpenApi.annotations({
|
|
87
|
+
summary: "Get aggregate health",
|
|
88
|
+
description: "Runs all registered liveness, readiness, and startup checks."
|
|
89
|
+
}))).add(HttpApiEndpoint.get("getLiveness", "/health/live", {
|
|
90
|
+
success: HealthUpResponse,
|
|
91
|
+
error: errors
|
|
92
|
+
}).annotateMerge(OpenApi.annotations({
|
|
93
|
+
summary: "Get application liveness",
|
|
94
|
+
description: "Returns whether the application process is responsive."
|
|
95
|
+
}))).add(HttpApiEndpoint.get("getReadiness", "/health/ready", {
|
|
96
|
+
success: HealthUpResponse,
|
|
97
|
+
error: errors
|
|
98
|
+
}).annotateMerge(OpenApi.annotations({
|
|
99
|
+
summary: "Get application readiness",
|
|
100
|
+
description: "Returns whether the application is ready to serve traffic."
|
|
101
|
+
}))).add(HttpApiEndpoint.get("getStartup", "/health/started", {
|
|
102
|
+
success: HealthUpResponse,
|
|
103
|
+
error: errors
|
|
104
|
+
}).annotateMerge(OpenApi.annotations({
|
|
105
|
+
summary: "Get application startup status",
|
|
106
|
+
description: "Returns whether the application has completed startup."
|
|
107
|
+
})));
|
|
108
|
+
|
|
109
|
+
// ../../src/services/health/index.ts
|
|
110
|
+
class HealthServiceConfig extends Context.Service()("@krak-stack/registry/HealthServiceConfig") {
|
|
111
|
+
static layerWith = ({
|
|
112
|
+
checks = {}
|
|
113
|
+
} = {}) => Layer.effect(this, Effect2.gen(function* () {
|
|
114
|
+
const services = yield* Effect2.context();
|
|
115
|
+
const resolve = (registered) => (registered ?? []).map(({ name, check }) => ({
|
|
116
|
+
name,
|
|
117
|
+
check: Effect2.provide(check, services)
|
|
118
|
+
}));
|
|
119
|
+
return {
|
|
120
|
+
live: resolve(checks.live),
|
|
121
|
+
ready: resolve(checks.ready),
|
|
122
|
+
started: resolve(checks.started)
|
|
123
|
+
};
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
class HealthService extends Context.Service()("@krak-stack/registry/HealthService", {
|
|
128
|
+
make: Effect2.gen(function* () {
|
|
129
|
+
const checks = yield* HealthServiceConfig;
|
|
130
|
+
const allChecks = Array.from(new Set([...checks.live, ...checks.ready, ...checks.started]));
|
|
131
|
+
const execute = Effect2.fn("HealthService.execute")(function* (healthCheck) {
|
|
132
|
+
const exit = yield* Effect2.exit(healthCheck.check);
|
|
133
|
+
if (!Exit.isSuccess(exit)) {
|
|
134
|
+
return {
|
|
135
|
+
name: healthCheck.name,
|
|
136
|
+
status: "DOWN"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return exit.value.data === undefined ? {
|
|
140
|
+
name: healthCheck.name,
|
|
141
|
+
status: exit.value.status
|
|
142
|
+
} : {
|
|
143
|
+
name: healthCheck.name,
|
|
144
|
+
status: exit.value.status,
|
|
145
|
+
data: exit.value.data
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
const run = Effect2.fn("HealthService.run")(function* (registered) {
|
|
149
|
+
const results = yield* Effect2.forEach(registered, execute, {
|
|
150
|
+
concurrency: "unbounded"
|
|
151
|
+
});
|
|
152
|
+
if (results.every(({ status }) => status === "UP")) {
|
|
153
|
+
return {
|
|
154
|
+
status: "UP",
|
|
155
|
+
checks: results
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
status: "DOWN",
|
|
160
|
+
checks: results
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
const aggregate = Effect2.fn("HealthService.aggregate")(() => run(allChecks));
|
|
164
|
+
const live = Effect2.fn("HealthService.live")(() => run(checks.live));
|
|
165
|
+
const ready = Effect2.fn("HealthService.ready")(() => run(checks.ready));
|
|
166
|
+
const started = Effect2.fn("HealthService.started")(() => run(checks.started));
|
|
167
|
+
return { aggregate, live, ready, started };
|
|
168
|
+
})
|
|
169
|
+
}) {
|
|
170
|
+
static up = (data) => data === undefined ? { status: "UP" } : { status: "UP", data };
|
|
171
|
+
static down = (data) => data === undefined ? { status: "DOWN" } : { status: "DOWN", data };
|
|
172
|
+
static layer = Layer.effect(this, this.make).pipe(Layer.provide(HealthServiceConfig.layerWith()));
|
|
173
|
+
static layerWith = (options) => Layer.effect(this, this.make).pipe(Layer.provide(HealthServiceConfig.layerWith(options)));
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
healthHandler,
|
|
177
|
+
HealthUpResponse,
|
|
178
|
+
HealthStatus,
|
|
179
|
+
HealthService,
|
|
180
|
+
HealthResponse,
|
|
181
|
+
HealthDownResponse,
|
|
182
|
+
HealthCheckResult,
|
|
183
|
+
HealthCheckOutcome,
|
|
184
|
+
HealthCheckData,
|
|
185
|
+
HealthApiGroup
|
|
186
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export declare const HealthStatus: Schema.Literals<readonly ["UP", "DOWN"]>;
|
|
3
|
+
export type HealthStatus = typeof HealthStatus.Type;
|
|
4
|
+
export declare const HealthCheckData: Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>>;
|
|
5
|
+
export type HealthCheckData = typeof HealthCheckData.Type;
|
|
6
|
+
export declare const HealthCheckOutcome: Schema.Struct<{
|
|
7
|
+
readonly status: Schema.Literals<readonly ["UP", "DOWN"]>;
|
|
8
|
+
readonly data: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>>>;
|
|
9
|
+
}>;
|
|
10
|
+
export type HealthCheckOutcome = typeof HealthCheckOutcome.Type;
|
|
11
|
+
export declare const HealthCheckResult: Schema.Struct<{
|
|
12
|
+
readonly name: Schema.NonEmptyString;
|
|
13
|
+
readonly status: Schema.Literals<readonly ["UP", "DOWN"]>;
|
|
14
|
+
readonly data: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>>>;
|
|
15
|
+
}>;
|
|
16
|
+
export type HealthCheckResult = typeof HealthCheckResult.Type;
|
|
17
|
+
export declare const HealthUpResponse: Schema.Struct<{
|
|
18
|
+
readonly status: Schema.Literal<"UP">;
|
|
19
|
+
readonly checks: Schema.$Array<Schema.Struct<{
|
|
20
|
+
readonly name: Schema.NonEmptyString;
|
|
21
|
+
readonly status: Schema.Literals<readonly ["UP", "DOWN"]>;
|
|
22
|
+
readonly data: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>>>;
|
|
23
|
+
}>>;
|
|
24
|
+
}>;
|
|
25
|
+
export type HealthUpResponse = typeof HealthUpResponse.Type;
|
|
26
|
+
export declare const HealthDownResponse: Schema.Struct<{
|
|
27
|
+
readonly status: Schema.Literal<"DOWN">;
|
|
28
|
+
readonly checks: Schema.$Array<Schema.Struct<{
|
|
29
|
+
readonly name: Schema.NonEmptyString;
|
|
30
|
+
readonly status: Schema.Literals<readonly ["UP", "DOWN"]>;
|
|
31
|
+
readonly data: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>>>;
|
|
32
|
+
}>>;
|
|
33
|
+
}>;
|
|
34
|
+
export type HealthDownResponse = typeof HealthDownResponse.Type;
|
|
35
|
+
export declare const HealthResponse: Schema.Union<readonly [Schema.Struct<{
|
|
36
|
+
readonly status: Schema.Literal<"UP">;
|
|
37
|
+
readonly checks: Schema.$Array<Schema.Struct<{
|
|
38
|
+
readonly name: Schema.NonEmptyString;
|
|
39
|
+
readonly status: Schema.Literals<readonly ["UP", "DOWN"]>;
|
|
40
|
+
readonly data: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>>>;
|
|
41
|
+
}>>;
|
|
42
|
+
}>, Schema.Struct<{
|
|
43
|
+
readonly status: Schema.Literal<"DOWN">;
|
|
44
|
+
readonly checks: Schema.$Array<Schema.Struct<{
|
|
45
|
+
readonly name: Schema.NonEmptyString;
|
|
46
|
+
readonly status: Schema.Literals<readonly ["UP", "DOWN"]>;
|
|
47
|
+
readonly data: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Union<readonly [Schema.String, Schema.Boolean, Schema.Number]>>>;
|
|
48
|
+
}>>;
|
|
49
|
+
}>]>;
|
|
50
|
+
export type HealthResponse = typeof HealthResponse.Type;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krak-stack/registry",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Tree-shakable KrakStack components and Effect services.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -199,6 +199,10 @@
|
|
|
199
199
|
"types": "./dist/services/file-extraction/schema.d.ts",
|
|
200
200
|
"import": "./dist/services/file-extraction/schema.js"
|
|
201
201
|
},
|
|
202
|
+
"./service-health": {
|
|
203
|
+
"types": "./dist/services/health/index.d.ts",
|
|
204
|
+
"import": "./dist/services/health/index.js"
|
|
205
|
+
},
|
|
202
206
|
"./tailwind.css": "./tailwind.css",
|
|
203
207
|
"./package.json": "./package.json"
|
|
204
208
|
},
|