@gjermundgaraba/effect-actions 0.11.0 → 0.13.0
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 +102 -41
- package/dist/Action.d.mts +31 -117
- package/dist/Action.mjs +14 -53
- package/dist/ActionCli.d.mts +60 -28
- package/dist/ActionCli.mjs +54 -48
- package/dist/ActionHttp.d.mts +18 -67
- package/dist/ActionHttp.mjs +20 -70
- package/dist/ActionMcp.d.mts +23 -29
- package/dist/ActionMcp.mjs +56 -114
- package/dist/ActionRpc.d.mts +147 -0
- package/dist/ActionRpc.mjs +113 -0
- package/dist/ActionToolkit.d.mts +28 -9
- package/dist/ActionToolkit.mjs +31 -4
- package/dist/Authentication.d.mts +15 -15
- package/dist/Authentication.mjs +42 -96
- package/dist/Testing.d.mts +30 -23
- package/dist/Testing.mjs +27 -44
- package/dist/{internal/authentication.d.mts → authentication/provider.d.mts} +41 -9
- package/dist/authentication/provider.mjs +12 -0
- package/dist/authentication/refusal.mjs +36 -0
- package/dist/{internal/cli.d.mts → cli/commands.d.mts} +16 -12
- package/dist/cli/commands.mjs +302 -0
- package/dist/{internal → contract}/call.d.mts +2 -2
- package/dist/contract/call.mjs +10 -0
- package/dist/{internal → contract}/errors.d.mts +13 -10
- package/dist/{internal → contract}/errors.mjs +9 -14
- package/dist/{internal → contract}/implementation.d.mts +37 -21
- package/dist/{internal → contract}/implementation.mjs +18 -49
- package/dist/contract/media.d.mts +13 -0
- package/dist/contract/media.mjs +18 -0
- package/dist/contract/rules.d.mts +25 -0
- package/dist/{internal/actions.mjs → contract/rules.mjs} +6 -42
- package/dist/{internal → http}/client.d.mts +7 -9
- package/dist/http/client.mjs +17 -0
- package/dist/http/declared.mjs +20 -0
- package/dist/http/schema-errors.mjs +9 -0
- package/dist/mcp/protocol.d.mts +18 -0
- package/dist/{internal/mcp.mjs → mcp/protocol.mjs} +29 -62
- package/dist/stdio/console.mjs +64 -0
- package/dist/{internal → testing}/memory.d.mts +1 -1
- package/dist/{internal → testing}/memory.mjs +5 -17
- package/dist/toolkit/tools.mjs +29 -0
- package/docs/Action-failures.md +47 -0
- package/docs/Action-in-process.md +93 -0
- package/docs/Action.md +211 -426
- package/docs/ActionCli-failures.md +104 -0
- package/docs/ActionCli.md +208 -293
- package/docs/ActionHttp-failures.md +83 -0
- package/docs/ActionHttp.md +205 -282
- package/docs/ActionMcp-failures.md +31 -0
- package/docs/ActionMcp.md +222 -271
- package/docs/ActionRpc-failures.md +21 -0
- package/docs/ActionRpc.md +179 -0
- package/docs/ActionToolkit-failures.md +15 -0
- package/docs/ActionToolkit.md +103 -119
- package/docs/Authentication-advanced.md +139 -0
- package/docs/Authentication-failures.md +99 -0
- package/docs/Authentication.md +143 -392
- package/docs/CONTEXT.md +40 -24
- package/docs/README.md +75 -35
- package/docs/Testing-failures.md +21 -0
- package/docs/Testing.md +193 -151
- package/docs/guarantees.md +35 -96
- package/docs/setup-failures.md +37 -0
- package/docs/setup.md +90 -92
- package/examples/README.md +68 -0
- package/examples/assistant/README.md +92 -0
- package/examples/assistant/src/agent.ts +92 -0
- package/examples/assistant/src/cli.ts +34 -0
- package/examples/assistant/src/commands.ts +25 -0
- package/examples/assistant/src/identity/Identity.ts +33 -0
- package/examples/assistant/src/identity/authorize.ts +24 -0
- package/examples/assistant/src/identity/handlers.ts +10 -0
- package/examples/assistant/src/identity/members.ts +9 -0
- package/examples/assistant/src/identity/permissions.ts +23 -0
- package/examples/assistant/src/scripted.ts +23 -0
- package/examples/assistant/src/tasks/Tasks.ts +63 -0
- package/examples/assistant/src/tasks/handlers.ts +25 -0
- package/examples/assistant/src/tasks/store.ts +96 -0
- package/examples/assistant/src/toolkit.ts +30 -0
- package/examples/assistant/test/commands.test.ts +57 -0
- package/examples/assistant/test/identity/identity.test.ts +43 -0
- package/examples/assistant/test/tasks/store.test.ts +48 -0
- package/examples/assistant/test/tasks/tasks.test.ts +75 -0
- package/examples/assistant/test/toolkit.test.ts +121 -0
- package/examples/kitchen-sink/README.md +318 -0
- package/examples/kitchen-sink/src/Actions.ts +19 -0
- package/examples/kitchen-sink/src/Api.ts +24 -0
- package/examples/kitchen-sink/src/Rpc.ts +12 -0
- package/examples/kitchen-sink/src/admin/Admin.ts +59 -0
- package/examples/kitchen-sink/src/admin/authenticate.ts +18 -0
- package/examples/kitchen-sink/src/admin/authorize.ts +27 -0
- package/examples/kitchen-sink/src/admin/grants.ts +22 -0
- package/examples/kitchen-sink/src/admin/handlers.ts +21 -0
- package/examples/kitchen-sink/src/admin/keys.ts +8 -0
- package/examples/kitchen-sink/src/agent.ts +47 -0
- package/examples/kitchen-sink/src/assistant/Assistant.ts +82 -0
- package/examples/kitchen-sink/src/assistant/conversations.ts +71 -0
- package/examples/kitchen-sink/src/assistant/demo-model.ts +33 -0
- package/examples/kitchen-sink/src/assistant/handlers.ts +126 -0
- package/examples/kitchen-sink/src/attachments/Attachments.ts +48 -0
- package/examples/kitchen-sink/src/attachments/download.ts +49 -0
- package/examples/kitchen-sink/src/attachments/handlers.ts +38 -0
- package/examples/kitchen-sink/src/attachments/store.ts +58 -0
- package/examples/kitchen-sink/src/browser.ts +33 -0
- package/examples/kitchen-sink/src/cli.ts +85 -0
- package/examples/kitchen-sink/src/config.ts +18 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +61 -0
- package/examples/kitchen-sink/src/identity/authenticate.ts +87 -0
- package/examples/kitchen-sink/src/identity/authorize.ts +16 -0
- package/examples/kitchen-sink/src/identity/directory.ts +52 -0
- package/examples/kitchen-sink/src/identity/handlers.ts +9 -0
- package/examples/kitchen-sink/src/identity/members.ts +13 -0
- package/examples/kitchen-sink/src/identity/quota.ts +62 -0
- package/examples/kitchen-sink/src/issues/Issues.ts +111 -0
- package/examples/kitchen-sink/src/issues/assistant.ts +23 -0
- package/examples/kitchen-sink/src/issues/guide.ts +37 -0
- package/examples/kitchen-sink/src/issues/handlers.ts +37 -0
- package/examples/kitchen-sink/src/issues/store.ts +96 -0
- package/examples/kitchen-sink/src/issues/write-limit.ts +42 -0
- package/examples/kitchen-sink/src/memory.ts +30 -0
- package/examples/kitchen-sink/src/ops.ts +42 -0
- package/examples/kitchen-sink/src/request-policy.ts +63 -0
- package/examples/kitchen-sink/src/routes.ts +120 -0
- package/examples/kitchen-sink/src/sdk.ts +46 -0
- package/examples/kitchen-sink/src/seed.ts +65 -0
- package/examples/kitchen-sink/src/server.ts +62 -0
- package/examples/kitchen-sink/src/stdio.ts +34 -0
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +28 -0
- package/examples/kitchen-sink/src/workspaces/handlers.ts +23 -0
- package/examples/kitchen-sink/src/workspaces/registry.ts +46 -0
- package/examples/kitchen-sink/src/workspaces/tenant.ts +17 -0
- package/examples/kitchen-sink/test/admin/admin.test.ts +67 -0
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +435 -0
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +161 -0
- package/examples/kitchen-sink/test/identity/identity.test.ts +137 -0
- package/examples/kitchen-sink/test/identity/session.test.ts +110 -0
- package/examples/kitchen-sink/test/issues/assistant.test.ts +75 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +135 -0
- package/examples/kitchen-sink/test/request-policy.test.ts +125 -0
- package/examples/kitchen-sink/test/routes.test.ts +123 -0
- package/examples/kitchen-sink/test/support/scripted.ts +40 -0
- package/examples/live-rpc/README.md +107 -0
- package/examples/live-rpc/src/Rpc.ts +8 -0
- package/examples/live-rpc/src/browser.ts +98 -0
- package/examples/live-rpc/src/chat/Chat.ts +77 -0
- package/examples/live-rpc/src/chat/authorize.ts +16 -0
- package/examples/live-rpc/src/chat/handlers.ts +50 -0
- package/examples/live-rpc/src/chat/room.ts +23 -0
- package/examples/live-rpc/src/chat/store.ts +54 -0
- package/examples/live-rpc/src/client.ts +37 -0
- package/examples/live-rpc/src/identity/Identity.ts +19 -0
- package/examples/live-rpc/src/identity/members.ts +8 -0
- package/examples/live-rpc/src/identity/verifier.ts +23 -0
- package/examples/live-rpc/src/routes.ts +22 -0
- package/examples/live-rpc/src/server.ts +43 -0
- package/examples/live-rpc/test/browser.test.ts +105 -0
- package/examples/live-rpc/test/chat/chat.test.ts +69 -0
- package/examples/live-rpc/test/routes.test.ts +99 -0
- package/examples/live-rpc/test/server.test.ts +75 -0
- package/examples/mcp-tools/README.md +141 -0
- package/examples/mcp-tools/src/instructions.ts +12 -0
- package/examples/mcp-tools/src/printers/Printers.ts +121 -0
- package/examples/mcp-tools/src/printers/fake.ts +115 -0
- package/examples/mcp-tools/src/printers/farm.ts +136 -0
- package/examples/mcp-tools/src/printers/handlers.ts +29 -0
- package/examples/mcp-tools/src/routes.ts +52 -0
- package/examples/mcp-tools/src/server.ts +31 -0
- package/examples/mcp-tools/src/stdio.ts +26 -0
- package/examples/mcp-tools/test/printers/printers.test.ts +189 -0
- package/examples/mcp-tools/test/routes.test.ts +202 -0
- package/examples/notes-service/README.md +111 -0
- package/examples/notes-service/src/Api.ts +13 -0
- package/examples/notes-service/src/browser.ts +41 -0
- package/examples/notes-service/src/cli.ts +22 -0
- package/examples/notes-service/src/commands.ts +137 -0
- package/examples/notes-service/src/identity/Identity.ts +31 -0
- package/examples/notes-service/src/identity/authorize.ts +18 -0
- package/examples/notes-service/src/identity/handlers.ts +10 -0
- package/examples/notes-service/src/identity/users.ts +9 -0
- package/examples/notes-service/src/identity/verifier.ts +36 -0
- package/examples/notes-service/src/notes/Notes.ts +119 -0
- package/examples/notes-service/src/notes/handlers.ts +39 -0
- package/examples/notes-service/src/notes/store.ts +84 -0
- package/examples/notes-service/src/routes.ts +45 -0
- package/examples/notes-service/src/server.ts +26 -0
- package/examples/notes-service/src/status/Status.ts +10 -0
- package/examples/notes-service/src/status/handlers.ts +14 -0
- package/examples/notes-service/src/stdio.ts +25 -0
- package/examples/notes-service/test/commands.test.ts +288 -0
- package/examples/notes-service/test/notes/notes.test.ts +103 -0
- package/examples/notes-service/test/routes.test.ts +84 -0
- package/examples/quickstart/README.md +31 -0
- package/examples/quickstart/src/Api.ts +5 -0
- package/examples/quickstart/src/client.ts +13 -0
- package/examples/quickstart/src/greeting/Greeting.ts +12 -0
- package/examples/quickstart/src/greeting/handlers.ts +6 -0
- package/examples/quickstart/src/routes.ts +11 -0
- package/examples/quickstart/src/server.ts +16 -0
- package/examples/quickstart/test/greeting/greeting.test.ts +18 -0
- package/package.json +27 -9
- package/dist/internal/actions.d.mts +0 -9
- package/dist/internal/authentication.mjs +0 -11
- package/dist/internal/call.mjs +0 -12
- package/dist/internal/cli.mjs +0 -328
- package/dist/internal/client.mjs +0 -25
- package/dist/internal/console.mjs +0 -77
- package/dist/internal/declared.d.mts +0 -13
- package/dist/internal/declared.mjs +0 -34
- package/dist/internal/mcp.d.mts +0 -14
- package/dist/internal/media.d.mts +0 -8
- package/dist/internal/media.mjs +0 -26
- package/dist/internal/refusal.mjs +0 -67
- package/dist/internal/schema-errors.mjs +0 -20
- package/dist/internal/tools.mjs +0 -44
package/dist/ActionCli.d.mts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
import { BuiltIn } from "./
|
|
2
|
-
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, RequestOf, Selected, SelectedOf, Selection, Served } from "./
|
|
3
|
-
import { Action_d_exports, Any } from "./Action.mjs";
|
|
4
|
-
import { VerifierError } from "./
|
|
5
|
-
import { Options, UserError } from "./
|
|
6
|
-
import { AnyHttp, Options as Options$2 } from "./
|
|
1
|
+
import { BuiltIn } from "./contract/errors.mjs";
|
|
2
|
+
import { ActionOf, BuildError, BuildServices, Holding, Known, Member, RequestOf, Selected, SelectedOf, Selection, Served } from "./contract/implementation.mjs";
|
|
3
|
+
import { Action_d_exports, Any, Errors } from "./Action.mjs";
|
|
4
|
+
import { VerifierError } from "./authentication/provider.mjs";
|
|
5
|
+
import { Options, UserError } from "./cli/commands.mjs";
|
|
6
|
+
import { AnyHttp, Options as Options$2 } from "./http/client.mjs";
|
|
7
7
|
import { Effect, Schema } from "effect";
|
|
8
8
|
import { HttpClient, HttpClientError } from "effect/http";
|
|
9
9
|
import { Command } from "effect/cli";
|
|
10
|
-
//#region src/ActionCli.d.ts
|
|
10
|
+
//#region src/cli/ActionCli.d.ts
|
|
11
|
+
interface AnyBinding {
|
|
12
|
+
readonly authentication?: {
|
|
13
|
+
readonly error: Errors;
|
|
14
|
+
} | undefined;
|
|
15
|
+
readonly actions: ReadonlyArray<Any>;
|
|
16
|
+
readonly error: Errors;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Make a command of your own fail as a derived command does. Give it what the command calls, the
|
|
20
|
+
* binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and pipe the command's
|
|
21
|
+
* whole Effect through it: a failure that is a declared error, the binding's and its
|
|
22
|
+
* authentication's included, or a built-in one becomes a `UserError` whose message is its JSON, and any other, a transport
|
|
23
|
+
* failure for one, a `UserError` naming it and the causes beneath it. An Effect CLI error, your
|
|
24
|
+
* own `new CliError.UserError({ cause, userMessage })` included, passes through as it is. To
|
|
25
|
+
* add context, such as how far a batch got, print it before this in the pipe, with
|
|
26
|
+
* `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result would
|
|
27
|
+
* replace its JSON.
|
|
28
|
+
*/
|
|
29
|
+
export declare const toUserError: (calls: AnyBinding | ReadonlyArray<Any>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, UserError<Exclude<E, import("effect/cli/CliError").CliError>> | Exclude<E, Exclude<E, import("effect/cli/CliError").CliError>>, R>;
|
|
11
30
|
/**
|
|
12
31
|
* An aggregate command of the actions `A`: its name, the actions that are its subcommands,
|
|
13
32
|
* and their options.
|
|
@@ -25,33 +44,32 @@ interface Options$1<A extends Any = Any> {
|
|
|
25
44
|
* `{ readFile: { positional: ["path"], render } }`.
|
|
26
45
|
*/
|
|
27
46
|
readonly commands?: { readonly [K in A as K["name"]]?: Options<K>; };
|
|
47
|
+
/**
|
|
48
|
+
* Commands of your own beside the derived ones, as `Command.withSubcommands` takes them:
|
|
49
|
+
* `[importLinks]`, each made with Effect CLI's `Command.make`. One fails as you make it fail:
|
|
50
|
+
* with a `CliError.UserError` for `Command.run` to print its `userMessage` on stderr. Written
|
|
51
|
+
* in the call, or in options held with `satisfies` this type, the list adds its commands'
|
|
52
|
+
* failures and services to the aggregate's type; in options annotated with this type, it adds
|
|
53
|
+
* none.
|
|
54
|
+
*/
|
|
55
|
+
readonly subcommands?: Subcommands | undefined;
|
|
28
56
|
}
|
|
29
|
-
|
|
57
|
+
type Subcommands = ReadonlyArray<Command.Command.SubcommandEntry>;
|
|
58
|
+
type OwnCommand<Own extends Subcommands> = Subcommands extends Own ? never : Own[number] extends (infer Entry) ? Entry extends Command.Command<infer _N, infer _I, infer _C, infer _E, infer _R> ? Entry : Entry extends Command.Command.SubcommandGroup<infer Commands> ? Commands[number] : never : never;
|
|
30
59
|
type CommandsOf<O> = O extends unknown ? "commands" extends keyof O ? NonNullable<O["commands" & keyof O]> : never : never;
|
|
31
|
-
/** The keys of any member of `C`. */
|
|
32
60
|
type KeysOf<C> = C extends unknown ? keyof C : never;
|
|
33
|
-
/** The options any member of `C` gives command `K`. */
|
|
34
61
|
type CommandOf<C, K> = C extends unknown ? (K extends keyof C ? C[K] : never) : never;
|
|
35
|
-
/**
|
|
36
|
-
* No option, no command beyond the actions `A` of the target, and no command's option, beyond
|
|
37
|
-
* `Keys`'s, in any member of `O`.
|
|
38
|
-
*/
|
|
39
62
|
type KnownOptions<O, Keys, A extends Any> = Known<O, Keys> & ([CommandsOf<O>] extends [never] ? unknown : {
|
|
40
63
|
readonly commands?: Known<CommandsOf<O>, { readonly [K in A["name"]]: Options<A>; }> & { readonly [K in KeysOf<CommandsOf<O>>]?: Known<CommandOf<CommandsOf<O>, K>, Options<Any>>; };
|
|
41
64
|
});
|
|
42
|
-
/** The implementation of `A` among `App`. */
|
|
43
65
|
type Owning<App, A extends Any> = App extends unknown ? A extends ActionOf<App> ? App : never : never;
|
|
44
|
-
/**
|
|
45
|
-
* What one local command of `A` runs: its implementation's builder and authorization, then
|
|
46
|
-
* its handler. Authorization may refuse and any handler fail with a built-in error, so
|
|
47
|
-
* every one fails with `Action.BuiltIn` too.
|
|
48
|
-
*/
|
|
49
66
|
type Local<App, A extends Any> = Effect.Effect<A["success"]["Type"], A["error"][number]["Type"] | BuildError<App, A> | BuiltIn, RequestOf<App, A> | BuildServices<App, A>>;
|
|
50
67
|
/**
|
|
51
68
|
* Project one implemented action into a native Effect CLI command that runs its handler in
|
|
52
69
|
* process, behind its implementation's authorization. It is named after the action in kebab
|
|
53
70
|
* case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
|
|
54
|
-
* as JSON when it is not a struct.
|
|
71
|
+
* as JSON when it is not a struct. The flag of an array of strings or numbers is repeated for each value, a boolean
|
|
72
|
+
* field is a switch, `--no-<name>` sending `false` as a required one left out does, and a field's `description` is its flag's help. It needs what the authorization, its handler and its builder
|
|
55
73
|
* need; the host provides the identity. It prints the result on stdout; a failure is a
|
|
56
74
|
* `UserError`, which `Command.run` prints on stderr. `remoteCommand` calls a server instead.
|
|
57
75
|
*/
|
|
@@ -75,24 +93,38 @@ export type RemoteCommandOptions<A extends Any = Any> = Options<A> & {
|
|
|
75
93
|
export type RemoteOptions<A extends Any = Any> = Options$1<A> & {
|
|
76
94
|
/**
|
|
77
95
|
* Every subcommand's client options, as `ActionHttp.client` takes them: `baseUrl`, and
|
|
78
|
-
* `transformClient` for credentials. They reach this aggregate's requests alone.
|
|
96
|
+
* `transformClient` for credentials. They reach this aggregate's requests alone. A URL or a
|
|
97
|
+
* token read from `Config` goes instead into the `HttpClient` provided on the command, with
|
|
98
|
+
* `Command.provideEffect(HttpClient.HttpClient, …)`: it is read when a command runs, so
|
|
99
|
+
* `--help` needs neither, and a command of your own in `subcommands` calls through it too.
|
|
100
|
+
* A required one that is not set fails that Effect with a `ConfigError`, which is no
|
|
101
|
+
* command's failure and prints as a crash line: map the read,
|
|
102
|
+
* `Effect.mapError((cause) => new CliError.UserError({ cause, userMessage: "TOKEN is not set" }))`.
|
|
79
103
|
*/
|
|
80
104
|
readonly client?: Options$2;
|
|
81
105
|
};
|
|
82
|
-
/** What an aggregate command over binding `H` fails with, running its actions `A`. */
|
|
83
106
|
type HttpUserError<H extends AnyHttp, A extends Any> = UserError<A["error"][number]["Type"] | H["error"][number]["Type"] | VerifierError<A, H["authentication"]>["Type"] | BuiltIn | HttpClientError.HttpClientError | Schema.SchemaError>;
|
|
84
107
|
/**
|
|
85
108
|
* Project every implemented action as a subcommand of one aggregate command, each named after
|
|
86
109
|
* its action in kebab case and run in process. `commands` gives a subcommand the options
|
|
87
|
-
* `command` takes, by action name
|
|
110
|
+
* `command` takes, by action name; `subcommands` adds commands of your own, which
|
|
111
|
+
* `Command.withSubcommands` on the result would put in the derived ones' place. `remote` calls a
|
|
112
|
+
* server instead. A test runs it in memory, with no process:
|
|
113
|
+
* `Command.runWith(command, { version })(args)`.
|
|
114
|
+
* Guide: `docs/ActionCli.md` in this package; in an application: `examples/notes-service/src/commands.ts`.
|
|
88
115
|
*/
|
|
89
|
-
export declare function make<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}>(implementations: Apps, options: O & NoInfer<Options$1<ActionOf<Member<Apps>>>> & NoInfer<KnownOptions<O, Options$1, ActionOf<Member<Apps>>>>
|
|
116
|
+
export declare function make<const Apps extends Served, const O extends Selection<ActionOf<Member<Apps>>> = {}, const Own extends Subcommands = []>(implementations: Apps, options: O & NoInfer<Options$1<ActionOf<Member<Apps>>>> & NoInfer<KnownOptions<O, Options$1, ActionOf<Member<Apps>>>> & {
|
|
117
|
+
readonly subcommands?: Own | undefined;
|
|
118
|
+
}): Command.Command<string, {}, {}, UserError<Effect.Error<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>>> | Command.Error<OwnCommand<Own>>, Effect.Services<Local<Holding<Member<Apps>, SelectedOf<O, Apps>>, SelectedOf<O, Apps>>> | Command.Services<OwnCommand<Own>>>;
|
|
90
119
|
/**
|
|
91
120
|
* Project every action of an HTTP binding as a subcommand of one aggregate command, derived as
|
|
92
|
-
* `make` derives it, each calling its action over HTTP. `client` configures every
|
|
93
|
-
* subcommand's client; `commands` gives
|
|
121
|
+
* `make` derives it, each calling its action over HTTP. `client` configures every derived
|
|
122
|
+
* subcommand's client; `commands` gives one the options `command` takes; `subcommands` adds
|
|
123
|
+
* commands of your own.
|
|
94
124
|
*/
|
|
95
|
-
export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}>(binding: H, options: O & NoInfer<RemoteOptions<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteOptions<Any>, H["actions"][number]>>
|
|
125
|
+
export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}, const Own extends Subcommands = []>(binding: H, options: O & NoInfer<RemoteOptions<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteOptions<Any>, H["actions"][number]>> & {
|
|
126
|
+
readonly subcommands?: Own | undefined;
|
|
127
|
+
}): Command.Command<string, {}, {}, HttpUserError<H, Selected<O, H["actions"][number]>> | Command.Error<OwnCommand<Own>>, HttpClient.HttpClient | Command.Services<OwnCommand<Own>>>;
|
|
96
128
|
/**
|
|
97
129
|
* A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
|
|
98
130
|
* subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
|
package/dist/ActionCli.mjs
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
|
-
import { assertDistinct, assertKnown } from "./
|
|
2
|
-
import { Implementation, acquire, assertHeld, select, toList } from "./
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { assertDistinct, assertKnown } from "./contract/rules.mjs";
|
|
2
|
+
import { Implementation, acquire, assertHeld, select, toList } from "./contract/implementation.mjs";
|
|
3
|
+
import { withVerifierErrors } from "./authentication/provider.mjs";
|
|
4
|
+
import { assertInBinding, methods } from "./http/client.mjs";
|
|
5
|
+
import { command as command$1, failingAsDerived } from "./cli/commands.mjs";
|
|
5
6
|
import { Cause, Effect, Logger, Runtime } from "effect";
|
|
6
7
|
import { Command } from "effect/cli";
|
|
7
|
-
//#region src/ActionCli.ts
|
|
8
|
+
//#region src/cli/ActionCli.ts
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Make a command of your own fail as a derived command does. Give it what the command calls, the
|
|
11
|
+
* binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and pipe the command's
|
|
12
|
+
* whole Effect through it: a failure that is a declared error, the binding's and its
|
|
13
|
+
* authentication's included, or a built-in one becomes a `UserError` whose message is its JSON, and any other, a transport
|
|
14
|
+
* failure for one, a `UserError` naming it and the causes beneath it. An Effect CLI error, your
|
|
15
|
+
* own `new CliError.UserError({ cause, userMessage })` included, passes through as it is. To
|
|
16
|
+
* add context, such as how far a batch got, print it before this in the pipe, with
|
|
17
|
+
* `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result would
|
|
18
|
+
* replace its JSON.
|
|
13
19
|
*/
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const toUserError = (calls) => "actions" in calls ? failingAsDerived(calls.actions, (action) => endpointErrorsOf(calls, action)) : failingAsDerived(calls, () => []);
|
|
21
|
+
const endpointErrorsOf = (binding, action) => withVerifierErrors(action, binding.error, binding.authentication?.error);
|
|
22
|
+
const runWithBuildOfItsOwn = (app, action, input) => {
|
|
23
|
+
const thisActionOnly = Implementation.share([action], app);
|
|
24
|
+
return Effect.flatMap(acquire([thisActionOnly]), (bound) => {
|
|
17
25
|
const [, run] = bound.find(([candidate]) => candidate === action) ?? [];
|
|
18
26
|
return run === void 0 ? Effect.die(/* @__PURE__ */ new Error(`No handler for ${action.name}`)) : run(input);
|
|
19
|
-
}).pipe(Effect.provide(Implementation.layerOf(
|
|
27
|
+
}).pipe(Effect.provide(Implementation.layerOf(thisActionOnly), { local: true }));
|
|
20
28
|
};
|
|
21
|
-
|
|
22
|
-
* The one implementation of `action` among `apps`. One implemented twice is refused rather
|
|
23
|
-
* than the first run; other actions' names are not this command's to check.
|
|
24
|
-
*/
|
|
25
|
-
const implementationOf = (apps, action) => {
|
|
29
|
+
const theImplementationOf = (apps, action) => {
|
|
26
30
|
const [app, ...others] = apps.filter((candidate) => candidate.actions.includes(action));
|
|
27
31
|
if (app === void 0) throw new Error(`Action "${action.name}" has no implementation here`);
|
|
28
32
|
if (others.length > 0) throw new Error(`Action "${action.name}" is implemented twice here`);
|
|
29
33
|
return app;
|
|
30
34
|
};
|
|
31
|
-
/** One command running `action` in process, by its one implementation among `apps`. */
|
|
32
35
|
const inProcess = (apps, action, options) => {
|
|
33
|
-
const app =
|
|
34
|
-
return command$1(action, (input) =>
|
|
36
|
+
const app = theImplementationOf(apps, action);
|
|
37
|
+
return command$1(action, (input) => runWithBuildOfItsOwn(app, action, input), options);
|
|
35
38
|
};
|
|
36
|
-
/**
|
|
37
|
-
* One command calling `action` through the binding's client, made with `client` on the
|
|
38
|
-
* host's `HttpClient`, whose failures include the binding's errors.
|
|
39
|
-
*/
|
|
40
39
|
const overHttp = (binding, action, options, client) => {
|
|
41
40
|
assertInBinding(binding.actions, action);
|
|
42
|
-
return command$1(action, (input) => Effect.flatMap(methods(binding, client), (methodOf) => methodOf(action)(input)), options, binding
|
|
41
|
+
return command$1(action, (input) => Effect.flatMap(methods(binding, client), (methodOf) => methodOf(action)(input)), options, endpointErrorsOf(binding, action));
|
|
43
42
|
};
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
*/
|
|
48
|
-
const aggregate = (name, actions, commands, project) => {
|
|
49
|
-
const subcommands = actions.map((action) => ({
|
|
50
|
-
action,
|
|
43
|
+
const aggregateCommand = (name, actions, commands, own, project) => {
|
|
44
|
+
const derived = actions.map((action) => ({
|
|
45
|
+
claimant: `action ${action.name}`,
|
|
51
46
|
command: project(action, Object.hasOwn(commands, action.name) ? commands[action.name] : void 0)
|
|
52
47
|
}));
|
|
53
|
-
assertDistinct("command",
|
|
54
|
-
|
|
48
|
+
assertDistinct("command", [...derived.map(({ claimant, command }) => ({
|
|
49
|
+
claimant,
|
|
50
|
+
name: command.name
|
|
51
|
+
})), ...own.flatMap((entry) => Command.isCommand(entry) ? [entry] : entry.commands).map((command) => ({
|
|
52
|
+
claimant: "subcommands",
|
|
53
|
+
name: command.name
|
|
54
|
+
}))], ({ name: claimed }) => claimed, ({ claimant }) => claimant);
|
|
55
|
+
return Command.make(name).pipe(Command.withSubcommands([...derived.map(({ command }) => command), ...own]));
|
|
55
56
|
};
|
|
56
|
-
|
|
57
|
-
const commandsOf = (commands, actions) => {
|
|
57
|
+
const checkedCommandsOf = (commands, actions) => {
|
|
58
58
|
const given = commands ?? {};
|
|
59
59
|
assertKnown("commands", Object.keys(given), actions.map((action) => action.name));
|
|
60
60
|
return given;
|
|
@@ -63,7 +63,8 @@ const commandsOf = (commands, actions) => {
|
|
|
63
63
|
* Project one implemented action into a native Effect CLI command that runs its handler in
|
|
64
64
|
* process, behind its implementation's authorization. It is named after the action in kebab
|
|
65
65
|
* case with one flag per field of its input (`--user-id`), or `--input` taking the whole input
|
|
66
|
-
* as JSON when it is not a struct.
|
|
66
|
+
* as JSON when it is not a struct. The flag of an array of strings or numbers is repeated for each value, a boolean
|
|
67
|
+
* field is a switch, `--no-<name>` sending `false` as a required one left out does, and a field's `description` is its flag's help. It needs what the authorization, its handler and its builder
|
|
67
68
|
* need; the host provides the identity. It prints the result on stdout; a failure is a
|
|
68
69
|
* `UserError`, which `Command.run` prints on stderr. `remoteCommand` calls a server instead.
|
|
69
70
|
*/
|
|
@@ -83,24 +84,29 @@ function remoteCommand(binding, action, options) {
|
|
|
83
84
|
/**
|
|
84
85
|
* Project every implemented action as a subcommand of one aggregate command, each named after
|
|
85
86
|
* its action in kebab case and run in process. `commands` gives a subcommand the options
|
|
86
|
-
* `command` takes, by action name
|
|
87
|
+
* `command` takes, by action name; `subcommands` adds commands of your own, which
|
|
88
|
+
* `Command.withSubcommands` on the result would put in the derived ones' place. `remote` calls a
|
|
89
|
+
* server instead. A test runs it in memory, with no process:
|
|
90
|
+
* `Command.runWith(command, { version })(args)`.
|
|
91
|
+
* Guide: `docs/ActionCli.md` in this package; in an application: `examples/notes-service/src/commands.ts`.
|
|
87
92
|
*/
|
|
88
93
|
function make(implementations, options) {
|
|
89
94
|
const apps = toList(implementations);
|
|
90
|
-
const commands =
|
|
95
|
+
const commands = checkedCommandsOf(options.commands, apps.flatMap((app) => app.actions));
|
|
91
96
|
const served = select(apps, options.actions);
|
|
92
|
-
return
|
|
97
|
+
return aggregateCommand(options.name, served.flatMap((app) => app.actions), commands, options.subcommands ?? [], (action, syntax) => inProcess(served, action, syntax));
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
95
100
|
* Project every action of an HTTP binding as a subcommand of one aggregate command, derived as
|
|
96
|
-
* `make` derives it, each calling its action over HTTP. `client` configures every
|
|
97
|
-
* subcommand's client; `commands` gives
|
|
101
|
+
* `make` derives it, each calling its action over HTTP. `client` configures every derived
|
|
102
|
+
* subcommand's client; `commands` gives one the options `command` takes; `subcommands` adds
|
|
103
|
+
* commands of your own.
|
|
98
104
|
*/
|
|
99
105
|
function remote(binding, options) {
|
|
100
106
|
const listed = options.actions;
|
|
101
107
|
if (listed !== void 0) assertHeld("the binding does not hold it", listed, binding.actions);
|
|
102
|
-
const commands =
|
|
103
|
-
return
|
|
108
|
+
const commands = checkedCommandsOf(options.commands, binding.actions);
|
|
109
|
+
return aggregateCommand(options.name, binding.actions.filter((action) => listed?.includes(action) ?? true), commands, options.subcommands ?? [], (action, syntax) => overHttp(binding, action, syntax, options.client));
|
|
104
110
|
}
|
|
105
111
|
/**
|
|
106
112
|
* A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
|
|
@@ -114,11 +120,11 @@ function remote(binding, options) {
|
|
|
114
120
|
const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
|
|
115
121
|
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
|
|
116
122
|
const squashed = Cause.squash(cause);
|
|
117
|
-
const
|
|
123
|
+
const reportedDefectKeepingCause = Object.assign(new Error("Reported on stderr", { cause }), {
|
|
118
124
|
[Runtime.errorReported]: false,
|
|
119
125
|
[Runtime.errorExitCode]: Runtime.getErrorExitCode(squashed)
|
|
120
126
|
});
|
|
121
|
-
return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(
|
|
127
|
+
return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(reportedDefectKeepingCause));
|
|
122
128
|
}), Effect.provideService(Logger.LogToStderr, true));
|
|
123
129
|
//#endregion
|
|
124
|
-
export { command, logToStderr, make, remote, remoteCommand };
|
|
130
|
+
export { command, logToStderr, make, remote, remoteCommand, toUserError };
|
package/dist/ActionHttp.d.mts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { Anyone } from "./
|
|
2
|
-
import { BuiltIn, BuiltIns } from "./
|
|
3
|
-
import { BuildError,
|
|
1
|
+
import { Anyone, Certain, ErrorsOf } from "./contract/rules.mjs";
|
|
2
|
+
import { BuiltIn, BuiltIns } from "./contract/errors.mjs";
|
|
3
|
+
import { BuildError, Holding, Known, Member, MiddlewareOf, Protected, Selected, Served, Serving } from "./contract/implementation.mjs";
|
|
4
4
|
import { Action_d_exports, Any, Errors } from "./Action.mjs";
|
|
5
|
-
import { Any as Any$1, Identity, Matching, RemoteRequest, Required, ServedProvider, VerifierError } from "./
|
|
6
|
-
import { AnyHttp, Client, MethodError, Options } from "./
|
|
7
|
-
import { ErrorsOf } from "./internal/declared.mjs";
|
|
5
|
+
import { Any as Any$1, DescriptorOf, Identity, Matching, PublicOnly, RemoteRequest, Required, ServedProvider, ServerBuildServices, VerifierError } from "./authentication/provider.mjs";
|
|
6
|
+
import { AnyHttp, Client, MethodError, Options } from "./http/client.mjs";
|
|
8
7
|
import { Context, Effect, FileSystem, Layer, Path, Schema, Types } from "effect";
|
|
9
8
|
import { Etag, HttpClient, HttpPlatform, HttpRouter } from "effect/http";
|
|
10
9
|
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/http-api";
|
|
11
|
-
//#region src/ActionHttp.d.ts
|
|
10
|
+
//#region src/http/ActionHttp.d.ts
|
|
12
11
|
/** What `fetchClient` takes: `client`'s options, and the `fetch` its calls send with. */
|
|
13
12
|
export interface FetchClientOptions extends Options {
|
|
14
13
|
/**
|
|
@@ -17,49 +16,13 @@ export interface FetchClientOptions extends Options {
|
|
|
17
16
|
*/
|
|
18
17
|
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
19
18
|
}
|
|
20
|
-
/** Native endpoint middleware, as `HttpApiMiddleware.Service` declares it. */
|
|
21
19
|
type Middleware = ReadonlyArray<Context.Key<HttpApiMiddleware.AnyId, unknown>>;
|
|
22
|
-
/** The identifier a middleware's service key declares. */
|
|
23
20
|
type IdOf<K> = K extends Context.Key<infer I extends HttpApiMiddleware.AnyId, unknown> ? I : never;
|
|
24
|
-
/**
|
|
25
|
-
* What the request owes past the middleware `M`, the first innermost, around what `R` owes:
|
|
26
|
-
* each removes what it provides and adds what it requires, as native endpoints apply them.
|
|
27
|
-
* An array of unknown length, which may hold none of them, provides nothing and owes what
|
|
28
|
-
* any of them requires; so does a slot that may hold one of several, as only one runs.
|
|
29
|
-
*/
|
|
30
21
|
type Through<M extends Middleware, R> = number extends M["length"] ? R | HttpApiMiddleware.Requires<IdOf<M[number]>> : M extends readonly [infer K, ...infer Rest extends Middleware] ? Through<Rest, true extends Types.IsUnion<IdOf<K>> ? R | HttpApiMiddleware.Requires<IdOf<K>> : HttpApiMiddleware.ApplyServices<IdOf<K>, R>> : R;
|
|
31
|
-
/**
|
|
32
|
-
* The middleware options `O` install: its `middleware` when always given, an array of unknown
|
|
33
|
-
* length when it may be absent, which installs none or all of them, and none otherwise.
|
|
34
|
-
*/
|
|
35
|
-
type MiddlewareOf<O> = O extends {
|
|
36
|
-
readonly middleware: infer M extends Middleware;
|
|
37
|
-
} ? M : O extends {
|
|
38
|
-
readonly middleware?: infer M extends Middleware | undefined;
|
|
39
|
-
} ? "middleware" extends keyof O ? ReadonlyArray<NonNullable<M>[number]> : [] : [];
|
|
40
|
-
/**
|
|
41
|
-
* The layer's middleware, refused when one fails with an error neither the binding, `E`, nor
|
|
42
|
-
* every endpoint declares, or needs a client counterpart: neither reaches the binding's
|
|
43
|
-
* clients, which decode only what the binding and the built-ins declare. A failure an
|
|
44
|
-
* action's callers see after decoding is its handler's, declared on the contract and decoded
|
|
45
|
-
* by every client.
|
|
46
|
-
*/
|
|
47
22
|
type ServerOnly<M extends Middleware, E extends Errors> = [Exclude<HttpApiMiddleware.Error<IdOf<M[number]>>, Extract<Certain<E>, Errors[number]>["Type"] | BuiltIn> | HttpApiMiddleware.MiddlewareClient<IdOf<M[number]>>] extends [never] ? unknown : {
|
|
48
23
|
readonly "Layer middleware fails only with the binding's errors and needs no client": never;
|
|
49
24
|
};
|
|
50
|
-
|
|
51
|
-
type Single<T> = true extends Types.IsUnion<T> ? never : T;
|
|
52
|
-
/**
|
|
53
|
-
* The errors a binding of errors `E` surely declares: each slot of one error in a list of
|
|
54
|
-
* fixed length, never one of an array of unknown length, which may be empty, nor of a list
|
|
55
|
-
* `E` may be one of several.
|
|
56
|
-
*/
|
|
57
|
-
type Certain<E extends Errors> = true extends Types.IsUnion<E> ? never : number extends E["length"] ? never : { readonly [K in keyof E]: Single<E[K]>; }[number];
|
|
58
|
-
/**
|
|
59
|
-
* The layer serving the actions `A` of `Apps` through the binding `H`, behind the middleware
|
|
60
|
-
* `M`: an implementation holding none of them is not built.
|
|
61
|
-
*/
|
|
62
|
-
type HttpLayer<H extends AnyHttp, Apps extends Served, M extends Middleware, A extends Any = H["actions"][number]> = Layer.Layer<never, BuildError<Holding<Member<Apps>, A>, A>, BuildServices<Holding<Member<Apps>, A>, A> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", LayerRequest<Member<Apps>, A, M>> | ServedProvider<Member<Apps>, A, H extends {
|
|
25
|
+
type HttpLayer<H extends AnyHttp, Apps extends Served, M extends Middleware, A extends Any = H["actions"][number]> = Layer.Layer<never, BuildError<Holding<Member<Apps>, A>, A>, ServerBuildServices<Holding<Member<Apps>, A>, A> | HttpRouter.HttpRouter | HttpRouter.Request.From<"Requires", LayerRequest<Member<Apps>, A, M>> | ServedProvider<Member<Apps>, A, H extends {
|
|
63
26
|
readonly authentication: infer D;
|
|
64
27
|
} ? D : never> | IdOf<M[number]> | Etag.Generator | FileSystem.FileSystem | HttpPlatform.HttpPlatform | Path.Path>;
|
|
65
28
|
/** `ActionHttp.layer`'s options. */
|
|
@@ -77,20 +40,9 @@ export interface LayerOptions<M extends Middleware = [], A extends Any = never>
|
|
|
77
40
|
*/
|
|
78
41
|
readonly middleware?: M;
|
|
79
42
|
}
|
|
80
|
-
/** What the layer serving `A` of `App` behind middleware `M` owes per request. */
|
|
81
43
|
type LayerRequest<App, A extends Any, M extends Middleware> = Exclude<Through<M, RemoteRequest<App, A>>, HttpRouter.Provided | ([Extract<Serving<App, A>, {
|
|
82
44
|
readonly caller: typeof Anyone;
|
|
83
45
|
}>] extends [never] ? Identity<Protected<Serving<App, A>>> : never)>;
|
|
84
|
-
/** Actions a binding takes without options: public ones, as a protected one names its descriptor. */
|
|
85
|
-
type PublicOnly<Actions extends ReadonlyArray<Any>> = [Exclude<Actions[number], {
|
|
86
|
-
readonly caller: typeof Anyone;
|
|
87
|
-
}>] extends [never] ? unknown : {
|
|
88
|
-
readonly "Protected actions take options naming their authentication": never;
|
|
89
|
-
};
|
|
90
|
-
/** The descriptor options `O` name, where they always name one. */
|
|
91
|
-
type DescriptorOf<O> = [O] extends [{
|
|
92
|
-
readonly authentication: infer D extends Any$1;
|
|
93
|
-
}] ? D : undefined;
|
|
94
46
|
/** Contract-level configuration shared by servers and clients. */
|
|
95
47
|
interface Options$1<E extends Errors = Errors> {
|
|
96
48
|
readonly prefix?: `/${string}`;
|
|
@@ -99,11 +51,6 @@ interface Options$1<E extends Errors = Errors> {
|
|
|
99
51
|
readonly authentication?: Any$1;
|
|
100
52
|
}
|
|
101
53
|
type Endpoint<A extends Any, E extends Errors, D> = A extends Any ? HttpApiEndpoint.HttpApiEndpoint<A["name"], "POST", `/${string}`, never, never, Schema.toCodecJson<A["input"]>, never, Schema.toCodecJson<A["success"]>, Schema.toCodecJson<A["error"][number] | E[number] | VerifierError<A, D> | BuiltIns>, never> : never;
|
|
102
|
-
/**
|
|
103
|
-
* The native `HttpApi` of `Actions`: one top-level group, so native client methods are
|
|
104
|
-
* not nested, named after the binding's mount path, `/` at the root, so bindings composed
|
|
105
|
-
* into one host API keep their own groups.
|
|
106
|
-
*/
|
|
107
54
|
type Api<Actions extends ReadonlyArray<Any>, E extends Errors, D> = HttpApi.HttpApi<"actions", HttpApiGroup.HttpApiGroup<string, Endpoint<Actions[number], E, D>, true>>;
|
|
108
55
|
/**
|
|
109
56
|
* An HTTP binding: actions, the errors every endpoint declares, and where they are mounted.
|
|
@@ -122,11 +69,12 @@ export interface Binding<Actions extends ReadonlyArray<Any>, E extends Errors =
|
|
|
122
69
|
}
|
|
123
70
|
/**
|
|
124
71
|
* Bind actions once, for servers and clients alike, each at `POST <prefix>/<action>`.
|
|
125
|
-
* Every endpoint declares its action's errors, the binding's `
|
|
72
|
+
* Every endpoint declares its action's errors, the binding's `error`, and the built-in
|
|
126
73
|
* `InvalidInput`, `Unauthenticated` and `Forbidden`, so clients decode each as a typed
|
|
127
74
|
* failure. A union's own `httpApiStatus` applies to every member; otherwise, each member
|
|
128
75
|
* uses its own status, or 422 without one. Protected contracts receive native endpoint
|
|
129
76
|
* security middleware: the same descriptor documents their credentials and enforces them.
|
|
77
|
+
* Guide: `docs/ActionHttp.md` in this package; in an application: `examples/notes-service/src/routes.ts`.
|
|
130
78
|
*/
|
|
131
79
|
export declare function make<const Actions extends ReadonlyArray<Any & {
|
|
132
80
|
readonly caller: typeof Anyone;
|
|
@@ -149,22 +97,25 @@ export declare function make<const Actions extends ReadonlyArray<Any>>(actions:
|
|
|
149
97
|
* need, until its `middleware` or middleware provided around it provides them, the identity
|
|
150
98
|
* of a protected action excepted, which its authentication provides; the router provides
|
|
151
99
|
* its own, such as the request, to every route. Its routes take only requests typed as JSON,
|
|
152
|
-
* and what middleware provides per request wins over what the layer was built with.
|
|
100
|
+
* and what middleware provides per request wins over what the layer was built with. A request
|
|
101
|
+
* body is read whole, however large, unless the host limits it where the routes are served:
|
|
102
|
+
* `Layer.provide(Layer.succeed(HttpServerRequest.MaxBodySize, ByteSize.mebibytes(1)))`.
|
|
153
103
|
*/
|
|
154
104
|
export declare function layer<const H extends AnyHttp, const Apps extends Served>(binding: H, implementations: Apps): HttpLayer<H, Apps, []>;
|
|
155
|
-
export declare function layer<const H extends AnyHttp, const Apps extends Served, const O extends LayerOptions<Middleware, H["actions"][number]>>(binding: H, implementations: Apps, options: O & ServerOnly<MiddlewareOf<O>, H["error"]> & NoInfer<Known<O, LayerOptions<Middleware>>>): HttpLayer<H, Apps, MiddlewareOf<O>, Selected<O, H["actions"][number]>>;
|
|
105
|
+
export declare function layer<const H extends AnyHttp, const Apps extends Served, const O extends LayerOptions<Middleware, H["actions"][number]>>(binding: H, implementations: Apps, options: O & ServerOnly<MiddlewareOf<O, Middleware>, H["error"]> & NoInfer<Known<O, LayerOptions<Middleware>>>): HttpLayer<H, Apps, MiddlewareOf<O, Middleware>, Selected<O, H["actions"][number]>>;
|
|
156
106
|
/**
|
|
157
107
|
* Effect's native `HttpApiClient` for a binding, one method per action taking the
|
|
158
108
|
* action's input directly: `client.greet({ name })`. The argument may be omitted when `{}`
|
|
159
109
|
* is a valid input. Requires the native `HttpClient`, as `HttpApiClient.make` does.
|
|
160
|
-
* The options are the native ones, `baseUrl` and `transformClient
|
|
161
|
-
*
|
|
110
|
+
* The options are the native ones, `baseUrl` and `transformClient`, a bearer token being
|
|
111
|
+
* `transformClient: HttpClient.mapRequest(HttpClientRequest.bearerToken(token))`. The native
|
|
112
|
+
* client itself stays available: `HttpApiClient.make(Http.api)`.
|
|
162
113
|
*
|
|
163
114
|
* A call fails with a declared error value (the action's own, or a built-in
|
|
164
115
|
* `InvalidInput`, `Unauthenticated` or `Forbidden`), a native `HttpClientError` when the
|
|
165
116
|
* server could not be reached or answered with a status or body the contract does not
|
|
166
|
-
* declare, or a `SchemaError` when the
|
|
167
|
-
*
|
|
117
|
+
* declare, or a `SchemaError` when the success does not decode. An input that does not encode
|
|
118
|
+
* fails with `InvalidInput`, and nothing is sent.
|
|
168
119
|
*/
|
|
169
120
|
export declare function client<const H extends AnyHttp>(binding: H, options?: Options): Effect.Effect<Client<H>, never, HttpClient.HttpClient>;
|
|
170
121
|
/**
|
package/dist/ActionHttp.mjs
CHANGED
|
@@ -1,61 +1,27 @@
|
|
|
1
|
-
import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./
|
|
2
|
-
import { acquire,
|
|
3
|
-
import { assertAuthentication } from "./
|
|
4
|
-
import { declared } from "./
|
|
5
|
-
import { methods } from "./
|
|
6
|
-
import { SchemaErrors, schemaErrors } from "./
|
|
1
|
+
import { Anyone, assertDistinct, assertOnce, assertOwnTags, errorList, projectedErrors } from "./contract/rules.mjs";
|
|
2
|
+
import { acquire, provideHandlers, servedBy, toList } from "./contract/implementation.mjs";
|
|
3
|
+
import { assertAuthentication, providerOf, withVerifierErrors } from "./authentication/provider.mjs";
|
|
4
|
+
import { declared } from "./http/declared.mjs";
|
|
5
|
+
import { methods } from "./http/client.mjs";
|
|
6
|
+
import { SchemaErrors, schemaErrors } from "./http/schema-errors.mjs";
|
|
7
7
|
import { Context, Effect, Layer, Scope } from "effect";
|
|
8
8
|
import { FetchHttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
9
9
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api";
|
|
10
|
-
//#region src/ActionHttp.ts
|
|
11
|
-
/** The mount path's segments: `api` by default; none for `/`. */
|
|
10
|
+
//#region src/http/ActionHttp.ts
|
|
12
11
|
const mountSegments = (prefix) => (prefix ?? "/api").split("/").filter((segment) => segment !== "");
|
|
13
|
-
|
|
14
|
-
const route = (segments) => `/${segments.join("/")}`;
|
|
15
|
-
/**
|
|
16
|
-
* The name of a binding's one native group: its OpenAPI tag, by which the native `addHttpApi`
|
|
17
|
-
* keys groups. It is the mount path, `/` at the root, which no segment contains.
|
|
18
|
-
*/
|
|
12
|
+
const absolutePath = (segments) => `/${segments.join("/")}`;
|
|
19
13
|
const groupName = (mount) => mount.join("/") || "/";
|
|
20
|
-
/** A native API of one top-level group of `endpoints`. */
|
|
21
14
|
const apiOf = (group, endpoints) => {
|
|
22
15
|
const empty = HttpApiGroup.make(group, { topLevel: true });
|
|
23
16
|
const [first, ...rest] = endpoints;
|
|
24
17
|
return HttpApi.make("actions").annotate(HttpApi.ParseOptions, { errors: "all" }).add(first === void 0 ? empty : empty.add(first, ...rest));
|
|
25
18
|
};
|
|
26
|
-
/**
|
|
27
|
-
* `apps` narrowed to the actions the layer serves, matched by identity: those `listed`, which
|
|
28
|
-
* the binding must hold, or every one of the binding's they hold, each once. An
|
|
29
|
-
* implementation holding none of them is left out, and not built. Implementations holding
|
|
30
|
-
* none of the binding's actions are refused, as the wrong implementations or the wrong
|
|
31
|
-
* binding; a name the binding holds is marked as another contract's, as a second copy of the
|
|
32
|
-
* contracts module makes one.
|
|
33
|
-
*/
|
|
34
|
-
const servedBy = (binding, apps, listed) => {
|
|
35
|
-
if (listed !== void 0) assertHeld("the binding does not hold it", listed, binding.actions);
|
|
36
|
-
const held = apps.flatMap((app) => app.actions);
|
|
37
|
-
const served = select(apps, listed ?? binding.actions.filter((action) => held.includes(action)));
|
|
38
|
-
const actions = served.flatMap((app) => app.actions);
|
|
39
|
-
if (actions.length === 0 && listed === void 0 && apps.length > 0) {
|
|
40
|
-
const bound = new Set(binding.actions.map(({ name }) => name));
|
|
41
|
-
throw new Error(`No action of these implementations is in this HTTP binding: ${held.map(({ name }) => bound.has(name) ? `${name} (another contract)` : name).join(", ") || "none"}`);
|
|
42
|
-
}
|
|
43
|
-
assertOnce("served action", actions);
|
|
44
|
-
return served;
|
|
45
|
-
};
|
|
46
|
-
/**
|
|
47
|
-
* The native endpoint of `action` at `path`, declaring its action's errors, the binding's
|
|
48
|
-
* `errors` and the built-in ones: what `make` documents and `layer` serves. Its payload is
|
|
49
|
-
* JSON whatever encoding the input is annotated with, which no other surface reads either:
|
|
50
|
-
* `HttpApi` would take a form or text body for it, which a page sends without a preflight.
|
|
51
|
-
*/
|
|
52
19
|
const endpointOf = (action, path, errors) => HttpApiEndpoint.post(action.name, path, {
|
|
53
20
|
payload: action.input.pipe(HttpApiSchema.asJson()),
|
|
54
21
|
success: action.success,
|
|
55
22
|
error: projectedErrors(action, errors).flatMap(declared)
|
|
56
23
|
});
|
|
57
|
-
|
|
58
|
-
const within = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
|
|
24
|
+
const withinLayerMiddleware = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
|
|
59
25
|
function make(actions, options = {}) {
|
|
60
26
|
assertOnce("action", actions);
|
|
61
27
|
const errors = errorList(options.error);
|
|
@@ -63,52 +29,37 @@ function make(actions, options = {}) {
|
|
|
63
29
|
assertAuthentication(actions, options.authentication);
|
|
64
30
|
const mount = mountSegments(options.prefix);
|
|
65
31
|
const security = options.authentication?.["~middleware"];
|
|
66
|
-
const verifierErrors = options.authentication?.error ?? [];
|
|
67
32
|
const endpoints = actions.map((action) => {
|
|
68
33
|
const isPublic = security === void 0 || action.caller === Anyone;
|
|
69
|
-
const endpoint = endpointOf(action,
|
|
34
|
+
const endpoint = endpointOf(action, absolutePath([...mount, action.name]), withVerifierErrors(action, errors, options.authentication?.error)).annotate(OpenApi.Description, action.description);
|
|
70
35
|
return isPublic ? endpoint : endpoint.middleware(security);
|
|
71
36
|
});
|
|
72
37
|
const api = apiOf(groupName(mount), endpoints);
|
|
73
38
|
return {
|
|
74
39
|
actions,
|
|
75
40
|
error: errors,
|
|
76
|
-
prefix:
|
|
41
|
+
prefix: absolutePath(mount),
|
|
77
42
|
api,
|
|
78
43
|
authentication: options.authentication
|
|
79
44
|
};
|
|
80
45
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* the context a group is built in over every request's, where a startup value would replace
|
|
84
|
-
* what middleware provides for the request. `entry` puts that context beneath it instead.
|
|
85
|
-
*/
|
|
86
|
-
const buildAlone = (group) => Layer.fromBuild((memoMap, scope) => Effect.setContext(Layer.buildWithMemoMap(group, memoMap, scope), Context.empty()));
|
|
87
|
-
/**
|
|
88
|
-
* Restore startup dependencies beneath request context, so a caller's services win.
|
|
89
|
-
*/
|
|
90
|
-
const entry = () => HttpRouter.middleware(Effect.map(Effect.context(), (built) => {
|
|
46
|
+
const buildInEmptyContext = (group) => Layer.fromBuild((memoMap, scope) => Effect.setContext(Layer.buildWithMemoMap(group, memoMap, scope), Context.empty()));
|
|
47
|
+
const startupBeneathRequest = () => HttpRouter.middleware(Effect.map(Effect.context(), (built) => {
|
|
91
48
|
const startup = Context.omit(Scope.Scope)(built);
|
|
92
49
|
return (route) => Effect.updateContext(route, (current) => Context.merge(startup, current));
|
|
93
50
|
})).layer;
|
|
94
|
-
/**
|
|
95
|
-
* Answers a protected route's step-up refusal, as it leaves the layer's middleware, with its
|
|
96
|
-
* challenge: the provider's `stepUp`, inside the authentication.
|
|
97
|
-
*/
|
|
98
51
|
var StepUp = class extends HttpApiMiddleware.Service()("effect-actions/ActionHttp/StepUp") {};
|
|
99
|
-
/** Require JSON media typing after endpoint authentication, before payload decoding. */
|
|
100
52
|
var JsonContentType = class extends HttpApiMiddleware.Service()("effect-actions/ActionHttp/JsonContentType") {};
|
|
101
|
-
/** `JsonContentType`'s server side, built by `layer` alone, so a client's bundle leaves it out. */
|
|
102
53
|
const jsonContentType = () => Layer.succeed(JsonContentType, (route) => Effect.flatMap(HttpServerRequest.HttpServerRequest, (request) => request.headers["content-type"] === void 0 ? Effect.succeed(HttpServerResponse.text("Unsupported content-type: none", { status: 415 })) : route));
|
|
103
54
|
function layer(binding, served, options = {}) {
|
|
104
|
-
const apps = servedBy(binding, toList(served), options.actions);
|
|
55
|
+
const apps = servedBy("HTTP binding", binding.actions, toList(served), options.actions);
|
|
105
56
|
const actions = apps.flatMap((app) => app.actions);
|
|
106
57
|
assertAuthentication(actions, binding.authentication);
|
|
107
58
|
assertDistinct("middleware", options.middleware ?? [], (key) => key.key);
|
|
108
59
|
const mount = mountSegments(binding.prefix);
|
|
109
60
|
const name = groupName(mount);
|
|
110
61
|
const api = apiOf(name, actions.map((action) => {
|
|
111
|
-
const endpoint =
|
|
62
|
+
const endpoint = withinLayerMiddleware(endpointOf(action, absolutePath([...mount, action.name]), binding.error).middleware(JsonContentType).middleware(SchemaErrors), options.middleware);
|
|
112
63
|
return action.caller === Anyone || binding.authentication === void 0 ? endpoint : endpoint.middleware(StepUp).middleware(binding.authentication["~middleware"]);
|
|
113
64
|
})).annotate(HttpApi.PayloadParseOptions, {
|
|
114
65
|
errors: "all",
|
|
@@ -117,19 +68,18 @@ function layer(binding, served, options = {}) {
|
|
|
117
68
|
const handlers = Layer.unwrap(Effect.gen(function* () {
|
|
118
69
|
const bound = yield* acquire(apps);
|
|
119
70
|
const auth = actions.some((action) => action.caller !== Anyone) ? binding.authentication : void 0;
|
|
120
|
-
const authentication = auth === void 0 ? Layer.empty : yield* Effect.
|
|
121
|
-
const
|
|
71
|
+
const authentication = auth === void 0 ? Layer.empty : yield* Effect.map(providerOf(auth), (provider) => Layer.mergeAll(Layer.succeed(auth["~middleware"], provider.middleware), Layer.succeed(StepUp, provider.stepUp)));
|
|
72
|
+
const hostProvidedMiddleware = yield* Effect.forEach(options.middleware ?? [], (key) => Effect.map(Effect.service(key), (service) => Layer.succeed(key, service)));
|
|
122
73
|
const byName = Object.fromEntries(bound.map(([action, run]) => [action.name, (request) => run(request.payload)]));
|
|
123
74
|
return HttpApiBuilder.group(api, name, (builder) => builder.handleAll(byName)).pipe(Layer.provide([
|
|
124
75
|
schemaErrors(),
|
|
125
76
|
jsonContentType(),
|
|
126
77
|
authentication,
|
|
127
|
-
...
|
|
128
|
-
]),
|
|
78
|
+
...hostProvidedMiddleware
|
|
79
|
+
]), buildInEmptyContext);
|
|
129
80
|
})).pipe(provideHandlers(apps));
|
|
130
|
-
return HttpApiBuilder.layer(api).pipe(Layer.provide(handlers), Layer.provide(
|
|
81
|
+
return HttpApiBuilder.layer(api).pipe(Layer.provide(handlers), Layer.provide(startupBeneathRequest()));
|
|
131
82
|
}
|
|
132
|
-
/** A binding's client, erased: `client` and `fetchClient` restore its exact type. */
|
|
133
83
|
const erasedClient = (binding, options) => Effect.map(methods(binding, options), (methodOf) => Object.fromEntries(binding.actions.map((action) => [action.name, methodOf(action)])));
|
|
134
84
|
function client(binding, options) {
|
|
135
85
|
return erasedClient(binding, options);
|