@gjermundgaraba/effect-actions 0.18.2 → 0.20.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 +20 -47
- package/dist/Action.d.mts +96 -49
- package/dist/Action.mjs +12 -16
- package/dist/ActionCli.d.mts +82 -44
- package/dist/ActionCli.mjs +54 -35
- package/dist/ActionHttp.d.mts +84 -45
- package/dist/ActionHttp.mjs +3 -4
- package/dist/ActionMcp.d.mts +54 -29
- package/dist/ActionMcp.mjs +33 -15
- package/dist/ActionRpc.d.mts +48 -37
- package/dist/ActionRpc.mjs +3 -4
- package/dist/ActionToolkit.d.mts +32 -15
- package/dist/ActionToolkit.mjs +11 -9
- package/dist/Authentication.d.mts +92 -42
- package/dist/Authentication.mjs +38 -37
- package/dist/Testing.d.mts +23 -27
- package/dist/Testing.mjs +1 -2
- package/dist/authentication/provider.d.mts +1 -6
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -3
- package/dist/cli/commands.d.mts +34 -13
- package/dist/cli/commands.mjs +48 -24
- package/dist/contract/ast.mjs +1 -2
- package/dist/contract/call.d.mts +1 -3
- package/dist/contract/call.mjs +1 -2
- package/dist/contract/caller.d.mts +1 -3
- package/dist/contract/caller.mjs +2 -3
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +28 -16
- package/dist/contract/errors.d.mts +42 -19
- package/dist/contract/errors.mjs +43 -19
- package/dist/contract/hints.mjs +4 -4
- package/dist/contract/implementation.d.mts +7 -14
- package/dist/contract/implementation.mjs +31 -19
- package/dist/contract/media.d.mts +7 -5
- package/dist/contract/media.mjs +7 -4
- package/dist/contract/names.mjs +15 -14
- package/dist/contract/status.mjs +4 -12
- package/dist/http/client.d.mts +15 -6
- package/dist/http/client.mjs +2 -3
- package/dist/http/schema-errors.mjs +1 -2
- package/dist/mcp/client.d.mts +21 -10
- package/dist/mcp/client.mjs +8 -11
- package/dist/mcp/protocol.mjs +1 -2
- package/dist/stdio/console.mjs +1 -2
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +1 -2
- package/docs/Action-failures.md +6 -15
- package/docs/Action.md +159 -191
- package/docs/ActionCli-failures.md +12 -92
- package/docs/ActionCli.md +102 -156
- package/docs/ActionHttp-failures.md +14 -76
- package/docs/ActionHttp.md +150 -176
- package/docs/ActionMcp-failures.md +2 -22
- package/docs/ActionMcp.md +79 -170
- package/docs/ActionRpc-failures.md +3 -12
- package/docs/ActionRpc.md +183 -120
- package/docs/ActionToolkit-failures.md +3 -10
- package/docs/ActionToolkit.md +112 -109
- package/docs/Authentication-advanced.md +171 -108
- package/docs/Authentication-failures.md +11 -91
- package/docs/Authentication.md +114 -135
- package/docs/README.md +45 -30
- package/docs/Testing-failures.md +2 -6
- package/docs/Testing.md +126 -159
- package/docs/setup-failures.md +8 -32
- package/docs/setup.md +42 -38
- package/examples/README.md +6 -10
- package/examples/assistant/README.md +20 -38
- package/examples/assistant/src/agent.ts +2 -3
- package/examples/assistant/src/cli.ts +4 -9
- package/examples/assistant/src/commands.ts +6 -4
- package/examples/assistant/src/identity/authorize.ts +3 -3
- package/examples/assistant/src/tasks/Tasks.ts +1 -2
- package/examples/assistant/src/tasks/handlers.ts +3 -3
- package/examples/assistant/src/toolkit.ts +6 -8
- package/examples/assistant/test/commands.test.ts +16 -11
- package/examples/assistant/test/tasks/tasks.test.ts +0 -2
- package/examples/assistant/test/toolkit.test.ts +0 -1
- package/examples/kitchen-sink/README.md +30 -52
- package/examples/kitchen-sink/src/Actions.ts +2 -3
- package/examples/kitchen-sink/src/Api.ts +3 -4
- package/examples/kitchen-sink/src/admin/Admin.ts +1 -1
- package/examples/kitchen-sink/src/admin/authenticate.ts +3 -3
- package/examples/kitchen-sink/src/admin/authorize.ts +1 -2
- package/examples/kitchen-sink/src/agent.ts +1 -1
- package/examples/kitchen-sink/src/assistant/handlers.ts +1 -6
- package/examples/kitchen-sink/src/attachments/Attachments.ts +4 -8
- package/examples/kitchen-sink/src/attachments/download.ts +5 -8
- package/examples/kitchen-sink/src/attachments/handlers.ts +2 -3
- package/examples/kitchen-sink/src/browser.ts +3 -4
- package/examples/kitchen-sink/src/cli.ts +6 -7
- package/examples/kitchen-sink/src/config.ts +2 -0
- package/examples/kitchen-sink/src/identity/Identity.ts +7 -7
- package/examples/kitchen-sink/src/identity/authenticate.ts +3 -5
- package/examples/kitchen-sink/src/identity/authorize.ts +4 -3
- package/examples/kitchen-sink/src/identity/quota.ts +1 -1
- package/examples/kitchen-sink/src/issues/Issues.ts +2 -2
- package/examples/kitchen-sink/src/issues/assistant.ts +3 -3
- package/examples/kitchen-sink/src/issues/guide.ts +2 -3
- package/examples/kitchen-sink/src/ops.ts +3 -6
- package/examples/kitchen-sink/src/request-policy.ts +8 -4
- package/examples/kitchen-sink/src/routes.ts +4 -6
- package/examples/kitchen-sink/src/seed.ts +0 -1
- package/examples/kitchen-sink/src/server.ts +11 -12
- package/examples/kitchen-sink/src/stdio.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/tenant.ts +3 -1
- package/examples/kitchen-sink/test/admin/admin.test.ts +0 -1
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +1 -1
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +1 -2
- package/examples/kitchen-sink/test/identity/identity.test.ts +0 -1
- package/examples/kitchen-sink/test/issues/issues.test.ts +0 -2
- package/examples/kitchen-sink/test/request-policy.test.ts +39 -3
- package/examples/kitchen-sink/test/routes.test.ts +3 -3
- package/examples/live-rpc/README.md +8 -49
- package/examples/live-rpc/src/browser.ts +8 -41
- package/examples/live-rpc/src/chat/Chat.ts +9 -3
- package/examples/live-rpc/src/chat/room.ts +1 -1
- package/examples/live-rpc/src/client.ts +2 -3
- package/examples/live-rpc/src/page.ts +34 -0
- package/examples/live-rpc/src/routes.ts +6 -7
- package/examples/live-rpc/src/server.ts +6 -4
- package/examples/live-rpc/test/browser.test.ts +6 -5
- package/examples/live-rpc/test/routes.test.ts +16 -15
- package/examples/live-rpc/test/server.test.ts +1 -1
- package/examples/mcp-tools/README.md +22 -45
- package/examples/mcp-tools/src/printers/Printers.ts +3 -7
- package/examples/mcp-tools/src/routes.ts +0 -7
- package/examples/mcp-tools/src/server.ts +1 -1
- package/examples/mcp-tools/test/printers/printers.test.ts +2 -3
- package/examples/mcp-tools/test/routes.test.ts +2 -3
- package/examples/notes-service/README.md +9 -43
- package/examples/notes-service/src/cli.ts +0 -3
- package/examples/notes-service/src/commands.ts +7 -17
- package/examples/notes-service/src/identity/authorize.ts +2 -5
- package/examples/notes-service/src/identity/verifier.ts +3 -4
- package/examples/notes-service/src/notes/Notes.ts +2 -6
- package/examples/notes-service/src/notes/store.ts +5 -1
- package/examples/notes-service/src/routes.ts +6 -8
- package/examples/notes-service/src/server.ts +4 -3
- package/examples/notes-service/src/stdio.ts +4 -3
- package/examples/notes-service/test/commands.test.ts +1 -1
- package/examples/notes-service/test/notes/as.ts +6 -0
- package/examples/notes-service/test/notes/idempotency.test.ts +1 -4
- package/examples/notes-service/test/notes/in-process.test.ts +50 -0
- package/examples/notes-service/test/notes/notes.test.ts +3 -42
- package/examples/notes-service/test/routes.test.ts +2 -2
- package/examples/quickstart/README.md +1 -1
- package/examples/quickstart/src/client.ts +1 -0
- package/examples/quickstart/src/server.ts +2 -4
- package/examples/quickstart/test/{greeting/greeting.test.ts → routes.test.ts} +3 -3
- package/package.json +12 -16
- package/docs/Action-in-process.md +0 -93
- package/docs/CONTEXT.md +0 -46
- package/docs/guarantees.md +0 -44
package/dist/ActionCli.d.mts
CHANGED
|
@@ -8,20 +8,22 @@ import "./ActionHttp.mjs";
|
|
|
8
8
|
import { Effect } from "effect";
|
|
9
9
|
import { HttpClient } from "effect/http";
|
|
10
10
|
import { CliError, Command } from "effect/cli";
|
|
11
|
-
//#region src/cli/ActionCli.d.ts
|
|
12
11
|
interface AnyBinding extends Declaring {
|
|
13
12
|
readonly actions: ReadonlyArray<Any>;
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
15
|
* Make a command of your own fail as a derived command does. Give it what the command calls, the
|
|
17
|
-
* binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and
|
|
18
|
-
* whole Effect through it: a failure that is a declared error, the binding's
|
|
19
|
-
* authentication's included, or a built-in one becomes a `UserError` whose message is its
|
|
20
|
-
* failure for one, a `UserError` naming it and the causes
|
|
21
|
-
* own `new CliError.UserError({ cause, userMessage })`
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
16
|
+
* binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and
|
|
17
|
+
* pipe the command's whole Effect through it: a failure that is a declared error, the binding's
|
|
18
|
+
* and its authentication's included, or a built-in one becomes a `UserError` whose message is its
|
|
19
|
+
* JSON, and any other, a transport failure for one, a `UserError` naming it and the causes
|
|
20
|
+
* beneath it. An Effect CLI error, your own `new CliError.UserError({ cause, userMessage })`
|
|
21
|
+
* included, passes through as it is.
|
|
22
|
+
*
|
|
23
|
+
* **Gotchas**
|
|
24
|
+
*
|
|
25
|
+
* - A failure left unmapped, one call's included, is reported as a crash, `ERROR (#n)` with a stack trace: pipe the whole Effect through it, once.
|
|
26
|
+
* - To add context, such as how far a batch got, print it before this in the pipe, `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result puts its JSON mid-sentence and hides the action's failure from `catchReason`.
|
|
25
27
|
*/
|
|
26
28
|
export declare const toUserError: (calls: AnyBinding | ReadonlyArray<Any>) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, UserError<Exclude<E, CliError.CliError>> | Exclude<E, Exclude<E, CliError.CliError>>, R>;
|
|
27
29
|
/**
|
|
@@ -43,11 +45,16 @@ interface Options$1<A extends Any = Any> {
|
|
|
43
45
|
readonly commands?: { readonly [K in A as K["name"]]?: Options<K>; };
|
|
44
46
|
/**
|
|
45
47
|
* Commands of your own beside the derived ones, as `Command.withSubcommands` takes them:
|
|
46
|
-
* `[importLinks]`, each made with Effect CLI's `Command.make
|
|
47
|
-
* with
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* `[importLinks]`, each made with Effect CLI's `Command.make` and printing its output itself,
|
|
49
|
+
* with `Console.log`. One calls actions through a client in its own handler: `yield*` the
|
|
50
|
+
* Effect `remote` takes as `client`, which needs no scope, or `Action.client`, acquired inside
|
|
51
|
+
* `Effect.scoped`. It fails as you make it fail: through `toUserError` as a derived command
|
|
52
|
+
* does, or with a `CliError.UserError` for `Command.run` to print its `userMessage` on stderr.
|
|
53
|
+
*
|
|
54
|
+
* **Gotchas**
|
|
55
|
+
*
|
|
56
|
+
* - Write the list in the call, or hold the options with `satisfies ActionCli.Options<…>`: in options annotated `: ActionCli.Options<…>` it adds none of its commands' failures and services to the aggregate's type, so the program compiles, then a command's service is not found.
|
|
57
|
+
* - Never pipe `Command.withSubcommands` onto the aggregate instead: it replaces the derived commands, which then answer with the usage text.
|
|
51
58
|
*/
|
|
52
59
|
readonly subcommands?: Subcommands | undefined;
|
|
53
60
|
}
|
|
@@ -64,25 +71,49 @@ type Local<App, A extends Any> = Effect.Effect<A["success"]["Type"], A["error"][
|
|
|
64
71
|
/**
|
|
65
72
|
* Project one implemented action into a native Effect CLI command that runs its handler in
|
|
66
73
|
* process, behind its implementation's authorization. It is named after the action in kebab
|
|
67
|
-
* case with one flag per field of its input (`--user-id`), or `--input` taking the
|
|
68
|
-
* as JSON when it is not a struct
|
|
69
|
-
*
|
|
70
|
-
* need
|
|
71
|
-
*
|
|
72
|
-
*
|
|
74
|
+
* case, with one flag per top-level field of its input (`--user-id`), or `--input` taking the
|
|
75
|
+
* whole input as JSON when it is not a struct of named fields. The host provides the caller on
|
|
76
|
+
* the command, `Command.provideSync`, beside what the authorization, the handler and the builder
|
|
77
|
+
* need. `remoteCommand` calls a server instead.
|
|
78
|
+
*
|
|
79
|
+
* **Gotchas**
|
|
80
|
+
*
|
|
81
|
+
* - A success is its JSON on stdout, indented by two spaces, or `render`'s text; nothing for an action without `success`. Its Effect logs and `Console` output go to stderr; other writes as `logToStderr` says.
|
|
82
|
+
* - A declared error is its JSON on stderr, under Effect CLI's `ERROR` heading, the command failing with a `UserError` whose `cause` is the error, exit 1 or its `Runtime.errorExitCode`; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say; input that does not decode, in `--input` or a flag's JSON value, is `InvalidInput`'s JSON there, while a missing flag or a rejected choice is Effect CLI's own parse error.
|
|
83
|
+
* - Effect CLI styles what it prints, a failure's JSON included, on a terminal and, from Effect 4.0.6, wherever `FORCE_COLOR` forces colour: for plain text there, set `NO_COLOR` or provide `TerminalCapabilities.TerminalCapabilities` with `canColor: false`.
|
|
84
|
+
* - A failure no schema encodes, a builder's or an accepted error that does not encode, is a `UserError` printed as its tag or name and its message, then its causes, never its other fields; a defect, an undeclared failure, or a success that does not encode is the command's defect, which `runMain` reports as `logToStderr` says.
|
|
85
|
+
* - Provide services with `Command.provide` on the command, built when an action runs: provided around `Command.run`, they are built before parsing, so `--help` and a mistyped flag open the database.
|
|
86
|
+
* - Each run builds the action's implementation, its builder and an authorizer an Effect builds, and releases them after the call, even where the host already built them; implementations passed beside it are not built.
|
|
87
|
+
* - A field named like a global flag (`help`, `version`, `log-level`) is no clash: its flag shadows the global one on this command.
|
|
73
88
|
*/
|
|
74
89
|
export declare function command<const Apps extends Served, A extends ActionOf<Member<Apps>>, const O = {}>(implementations: Apps, action: A, options?: O & NoInfer<Options<A>> & NoInfer<Known<O, Options<Any>>>): Command.Command<string, never, {}, Failing<Effect.Error<Local<Owning<Member<Apps>, A>, A>>>, Effect.Services<Local<Owning<Member<Apps>, A>, A>>>;
|
|
75
90
|
/**
|
|
76
91
|
* Project one action of an HTTP binding into a native Effect CLI command, derived as `command`
|
|
77
92
|
* derives it, that calls the action through `client`'s method and fails as the method does, or
|
|
78
|
-
* as the Effect building the client does.
|
|
79
|
-
*
|
|
80
|
-
* `ActionHttp.client(binding)` on the host's `HttpClient`. It runs nothing locally.
|
|
93
|
+
* as the Effect building the client does. It runs nothing locally and names no caller: the
|
|
94
|
+
* server verifies the credentials its client sends.
|
|
81
95
|
*/
|
|
82
|
-
export declare function remoteCommand<const H extends AnyHttp, A extends H["actions"][number], const O = {}, C extends ClientOf<H> | undefined = undefined>(binding: H, action: A, options?: O & NoInfer<Options<A>> & NoInfer<Known<O, RemoteKeys<Options<Any>>>> & {
|
|
96
|
+
export declare function remoteCommand<const H extends AnyHttp, A extends H["actions"][number], const O = {}, C extends ClientOf<H, A> | undefined = undefined>(binding: H, action: A, options?: O & NoInfer<Options<A>> & NoInfer<Known<O, RemoteKeys<Options<Any>>>> & Calling<C>): Command.Command<string, never, {}, Failing<CallFailure<H, C, A>>, CallServices<C, A>>;
|
|
97
|
+
export interface Calling<C> {
|
|
98
|
+
/**
|
|
99
|
+
* The connection: an Effect of the binding's client, such as
|
|
100
|
+
* `ActionHttp.client(Http, { baseUrl, transformClient })`, run each time a command runs, so one
|
|
101
|
+
* reading `Config` is an `Effect.gen` returning `yield* ActionHttp.client(…)` and `--help`
|
|
102
|
+
* reads nothing. What it requires, the host provides. Omitted, it is
|
|
103
|
+
* `ActionHttp.client(binding)` on the host's `HttpClient`, with no `baseUrl`. It needs only the
|
|
104
|
+
* methods of the actions it derives commands for: those `remote`'s `actions` lists, or
|
|
105
|
+
* `remoteCommand`'s one.
|
|
106
|
+
*
|
|
107
|
+
* **Gotchas**
|
|
108
|
+
*
|
|
109
|
+
* - A token read with `Config` and not set fails every command, a public one's too, as `ConfigError: …`, which names no fix: map it to a tagged error whose `message` says what to do, or read an optional one with `Config.option`.
|
|
110
|
+
* - Credentials go in `transformClient`: a command adds no headers of its own, so without them a protected action answers `Unauthenticated`.
|
|
111
|
+
* - Never transform the `HttpClient` around the program instead: it rewrites every request the program makes, an absolute URL too, and sends each one the token.
|
|
112
|
+
*/
|
|
83
113
|
readonly client?: C;
|
|
84
|
-
}
|
|
85
|
-
type ClientOf<H extends AnyHttp> = Effect.Effect<Client<H, unknown, unknown>, unknown, unknown>;
|
|
114
|
+
}
|
|
115
|
+
type ClientOf<H extends AnyHttp, A extends Any> = Effect.Effect<Pick<Client<H, unknown, unknown>, A["name"]>, unknown, unknown>;
|
|
116
|
+
type Needed<O, L extends ReadonlyArray<Any>, All extends Any> = "actions" extends (O extends unknown ? keyof O : never) ? Selected<O, All> : L[number];
|
|
86
117
|
type RemoteKeys<Keys> = Keys & {
|
|
87
118
|
readonly client?: unknown;
|
|
88
119
|
};
|
|
@@ -91,36 +122,43 @@ type Called<M, A extends Any> = A extends unknown ? A["name"] extends keyof M ?
|
|
|
91
122
|
type CallFailure<H extends AnyHttp, C, A extends Any> = C extends Effect.Effect<infer M, infer CE, unknown> ? CE | Effect.Error<Called<M, A>> : Effect.Error<Called<Client<H>, A>>;
|
|
92
123
|
type CallServices<C, A extends Any> = C extends Effect.Effect<infer M, unknown, infer CR> ? CR | Effect.Services<Called<M, A>> : HttpClient.HttpClient;
|
|
93
124
|
/**
|
|
94
|
-
* Project
|
|
95
|
-
*
|
|
96
|
-
* `command` takes, by action name; `subcommands` adds commands of your
|
|
97
|
-
*
|
|
98
|
-
* server instead. A test runs it in memory, with no process:
|
|
99
|
-
* `Command.runWith(command, { version })(args)`.
|
|
125
|
+
* Project the implemented actions, every one or those `actions` selects, as subcommands of one
|
|
126
|
+
* aggregate command, each derived as `command` derives it and run in process. `commands` gives a
|
|
127
|
+
* subcommand the options `command` takes, by action name; `subcommands` adds commands of your
|
|
128
|
+
* own. `remote` calls a server instead. A test runs it in memory, with no process: `Command.runWith(command, { version })(args)`.
|
|
100
129
|
* Guide: `docs/ActionCli.md` in this package; in an application: `examples/assistant/src/commands.ts`.
|
|
130
|
+
*
|
|
131
|
+
* **Gotchas**
|
|
132
|
+
*
|
|
133
|
+
* - Services provided on it with `Command.provide` are also built when it runs with no subcommand, before it shows its help.
|
|
101
134
|
*/
|
|
102
135
|
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>>>> & {
|
|
103
136
|
readonly subcommands?: Own | undefined;
|
|
104
137
|
}): Command.Command<string, {}, {}, Failing<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>>>;
|
|
105
138
|
/**
|
|
106
|
-
* Project
|
|
107
|
-
* `make` derives
|
|
108
|
-
* does. `commands` gives one the options `command` takes;
|
|
109
|
-
* own, which call through the same client by yielding the
|
|
139
|
+
* Project the actions of an HTTP binding, every one or those `actions` selects, as subcommands of
|
|
140
|
+
* one aggregate command, derived as `make` derives them, each calling its action through
|
|
141
|
+
* `client`'s method, as `remoteCommand` does. `commands` gives one the options `command` takes;
|
|
142
|
+
* `subcommands` adds commands of your own, which call through the same client by yielding the
|
|
143
|
+
* Effect given as `client`.
|
|
144
|
+
* In an application: `examples/notes-service/src/commands.ts`.
|
|
110
145
|
*/
|
|
111
|
-
export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}, const Own extends Subcommands = [], C extends ClientOf<H
|
|
112
|
-
readonly
|
|
146
|
+
export declare function remote<const H extends AnyHttp, const O extends Selection<H["actions"][number]> = {}, const Own extends Subcommands = [], const L extends ReadonlyArray<H["actions"][number]> = H["actions"], C extends ClientOf<H, Needed<O, L, H["actions"][number]>> | undefined = undefined>(binding: H, options: O & NoInfer<Options$1<H["actions"][number]>> & NoInfer<KnownOptions<O, RemoteKeys<Options$1<Any>>, H["actions"][number]>> & Calling<C> & {
|
|
147
|
+
readonly actions?: L | undefined;
|
|
113
148
|
readonly subcommands?: Own | undefined;
|
|
114
149
|
}): Command.Command<string, {}, {}, Failing<CallFailure<H, C, Selected<O, H["actions"][number]>>> | Command.Error<OwnCommand<Own>>, CallServices<C, Selected<O, H["actions"][number]>> | Command.Services<OwnCommand<Own>>>;
|
|
115
150
|
/**
|
|
116
151
|
* A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
|
|
117
152
|
* subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
|
|
118
|
-
* provided within writes to stderr, and a failure `runMain` would report, such as a
|
|
119
|
-
* a failure of a layer the host provides, is reported
|
|
120
|
-
* own failure, which `Command.run` has printed, is not
|
|
121
|
-
* one `runMain` gives: the failure's
|
|
122
|
-
* interruption.
|
|
153
|
+
* provided within writes to stderr, and a failure `runMain` would report on stdout, such as a
|
|
154
|
+
* defect, a success that does not encode or a failure of a layer the host provides, is reported
|
|
155
|
+
* on stderr instead, once. A command's own failure, which `Command.run` has printed, is not
|
|
156
|
+
* reported again. The exit code is the one `runMain` gives: the failure's
|
|
157
|
+
* `Runtime.errorExitCode`, 1 by default, or 130 for an interruption.
|
|
158
|
+
*
|
|
159
|
+
* **Gotchas**
|
|
160
|
+
*
|
|
161
|
+
* - It moves the default logger only: a host layer's `Console` output, the global `console.log`, and a logger writing through `Console.log` such as `Logger.consoleJson`, still reach stdout; log JSON with `Logger.withConsoleError(Logger.formatJson)`.
|
|
123
162
|
*/
|
|
124
163
|
export declare const logToStderr: <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, never, R>;
|
|
125
|
-
//#endregion
|
|
126
164
|
export { type Options as CommandOptions, Options$1 as Options, type UserError };
|
package/dist/ActionCli.mjs
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { assertDistinct, assertKnown } from "./contract/names.mjs";
|
|
2
2
|
import { bindingErrors, projectedErrors } from "./contract/declared.mjs";
|
|
3
|
-
import { Implementation, acquire, assertHeld, select, toList } from "./contract/implementation.mjs";
|
|
3
|
+
import { Implementation, acquire, assertHeld, notHeld, select, toList } from "./contract/implementation.mjs";
|
|
4
4
|
import { client } from "./ActionHttp.mjs";
|
|
5
5
|
import { command as command$1, failingAsDerived } from "./cli/commands.mjs";
|
|
6
6
|
import { Cause, Effect, Logger, Runtime } from "effect";
|
|
7
7
|
import { Command } from "effect/cli";
|
|
8
|
-
|
|
8
|
+
// src/cli/ActionCli.ts
|
|
9
9
|
/**
|
|
10
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
|
|
12
|
-
* whole Effect through it: a failure that is a declared error, the binding's
|
|
13
|
-
* authentication's included, or a built-in one becomes a `UserError` whose message is its
|
|
14
|
-
* failure for one, a `UserError` naming it and the causes
|
|
15
|
-
* own `new CliError.UserError({ cause, userMessage })`
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* binding of `ActionHttp.client` or `ActionRpc.client`, or the actions of `Action.client`, and
|
|
12
|
+
* pipe the command's whole Effect through it: a failure that is a declared error, the binding's
|
|
13
|
+
* and its authentication's included, or a built-in one becomes a `UserError` whose message is its
|
|
14
|
+
* JSON, and any other, a transport failure for one, a `UserError` naming it and the causes
|
|
15
|
+
* beneath it. An Effect CLI error, your own `new CliError.UserError({ cause, userMessage })`
|
|
16
|
+
* included, passes through as it is.
|
|
17
|
+
*
|
|
18
|
+
* **Gotchas**
|
|
19
|
+
*
|
|
20
|
+
* - A failure left unmapped, one call's included, is reported as a crash, `ERROR (#n)` with a stack trace: pipe the whole Effect through it, once.
|
|
21
|
+
* - To add context, such as how far a batch got, print it before this in the pipe, `Effect.tapError(() => Console.error(…))`: a `UserError` wrapped around the result puts its JSON mid-sentence and hides the action's failure from `catchReason`.
|
|
19
22
|
*/
|
|
20
23
|
const toUserError = (calls) => "actions" in calls ? failingAsDerived(calls.actions.flatMap((action) => bindingErrors(action, calls))) : failingAsDerived(calls.flatMap((action) => projectedErrors(action)));
|
|
21
24
|
const runWithBuildOfItsOwn = (app, action, input) => {
|
|
@@ -27,8 +30,8 @@ const runWithBuildOfItsOwn = (app, action, input) => {
|
|
|
27
30
|
};
|
|
28
31
|
const theImplementationOf = (apps, action) => {
|
|
29
32
|
const [app, ...others] = apps.filter((candidate) => candidate.actions.includes(action));
|
|
30
|
-
if (app === void 0) throw new Error(`Action "${action.name}" has no implementation here`);
|
|
31
|
-
if (others.length > 0) throw new Error(`Action "${action.name}" is implemented twice here`);
|
|
33
|
+
if (app === void 0) throw new Error(`Action "${action.name}" has no implementation here (pass its implementation, and select with the contract value it implements: an equal-looking action does not match)`);
|
|
34
|
+
if (others.length > 0) throw new Error(`Action "${action.name}" is implemented twice here (pass one of them)`);
|
|
32
35
|
return app;
|
|
33
36
|
};
|
|
34
37
|
const inProcess = (apps, action, options) => {
|
|
@@ -36,10 +39,10 @@ const inProcess = (apps, action, options) => {
|
|
|
36
39
|
return command$1(action, (input) => runWithBuildOfItsOwn(app, action, input), options, projectedErrors(action));
|
|
37
40
|
};
|
|
38
41
|
const overHttp = (binding, action, options, client$1) => {
|
|
39
|
-
if (!binding.actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding`);
|
|
42
|
+
if (!binding.actions.includes(action)) throw new Error(`Action "${action.name}" is not in this HTTP binding (pass it to the binding's ActionHttp.make, and select with the contract value it received)`);
|
|
40
43
|
return command$1(action, (input) => Effect.flatMap(client$1 ?? client(binding), (methods) => {
|
|
41
44
|
const method = methods[action.name];
|
|
42
|
-
if (method === void 0) throw new Error(`No client method for ${action.name}`);
|
|
45
|
+
if (method === void 0) throw new Error(`No client method for ${action.name} (give \`client\` a method for this action)`);
|
|
43
46
|
return method(input);
|
|
44
47
|
}), options, bindingErrors(action, binding));
|
|
45
48
|
};
|
|
@@ -54,23 +57,34 @@ const aggregateCommand = (name, actions, commands, own, project) => {
|
|
|
54
57
|
})), ...own.flatMap((entry) => Command.isCommand(entry) ? [entry] : entry.commands).map((command) => ({
|
|
55
58
|
claimant: "subcommands",
|
|
56
59
|
name: command.name
|
|
57
|
-
}))], ({ name: claimed }) => claimed,
|
|
60
|
+
}))], ({ name: claimed }) => claimed, {
|
|
61
|
+
claimantOf: ({ claimant }) => claimant,
|
|
62
|
+
fix: "rename one of your subcommands, or give an action's command another name in commands"
|
|
63
|
+
});
|
|
58
64
|
return Command.make(name).pipe(Command.withSubcommands([...derived.map(({ command }) => command), ...own]));
|
|
59
65
|
};
|
|
60
66
|
const checkedCommandsOf = (commands, actions) => {
|
|
61
67
|
const given = commands ?? {};
|
|
62
|
-
assertKnown("commands", Object.keys(given), actions.map((action) => action.name));
|
|
68
|
+
assertKnown("commands", Object.keys(given), actions.map((action) => action.name), "key it by the action's own name, not its command's");
|
|
63
69
|
return given;
|
|
64
70
|
};
|
|
65
71
|
/**
|
|
66
72
|
* Project one implemented action into a native Effect CLI command that runs its handler in
|
|
67
73
|
* process, behind its implementation's authorization. It is named after the action in kebab
|
|
68
|
-
* case with one flag per field of its input (`--user-id`), or `--input` taking the
|
|
69
|
-
* as JSON when it is not a struct
|
|
70
|
-
*
|
|
71
|
-
* need
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
+
* case, with one flag per top-level field of its input (`--user-id`), or `--input` taking the
|
|
75
|
+
* whole input as JSON when it is not a struct of named fields. The host provides the caller on
|
|
76
|
+
* the command, `Command.provideSync`, beside what the authorization, the handler and the builder
|
|
77
|
+
* need. `remoteCommand` calls a server instead.
|
|
78
|
+
*
|
|
79
|
+
* **Gotchas**
|
|
80
|
+
*
|
|
81
|
+
* - A success is its JSON on stdout, indented by two spaces, or `render`'s text; nothing for an action without `success`. Its Effect logs and `Console` output go to stderr; other writes as `logToStderr` says.
|
|
82
|
+
* - A declared error is its JSON on stderr, under Effect CLI's `ERROR` heading, the command failing with a `UserError` whose `cause` is the error, exit 1 or its `Runtime.errorExitCode`; a refusal as `Action.Unauthenticated` and `Action.Forbidden` say; input that does not decode, in `--input` or a flag's JSON value, is `InvalidInput`'s JSON there, while a missing flag or a rejected choice is Effect CLI's own parse error.
|
|
83
|
+
* - Effect CLI styles what it prints, a failure's JSON included, on a terminal and, from Effect 4.0.6, wherever `FORCE_COLOR` forces colour: for plain text there, set `NO_COLOR` or provide `TerminalCapabilities.TerminalCapabilities` with `canColor: false`.
|
|
84
|
+
* - A failure no schema encodes, a builder's or an accepted error that does not encode, is a `UserError` printed as its tag or name and its message, then its causes, never its other fields; a defect, an undeclared failure, or a success that does not encode is the command's defect, which `runMain` reports as `logToStderr` says.
|
|
85
|
+
* - Provide services with `Command.provide` on the command, built when an action runs: provided around `Command.run`, they are built before parsing, so `--help` and a mistyped flag open the database.
|
|
86
|
+
* - Each run builds the action's implementation, its builder and an authorizer an Effect builds, and releases them after the call, even where the host already built them; implementations passed beside it are not built.
|
|
87
|
+
* - A field named like a global flag (`help`, `version`, `log-level`) is no clash: its flag shadows the global one on this command.
|
|
74
88
|
*/
|
|
75
89
|
function command(implementations, action, options) {
|
|
76
90
|
return inProcess(toList(implementations), action, options);
|
|
@@ -80,13 +94,15 @@ function remoteCommand(binding, action, options) {
|
|
|
80
94
|
return overHttp(binding, action, options === void 0 ? void 0 : syntax, client);
|
|
81
95
|
}
|
|
82
96
|
/**
|
|
83
|
-
* Project
|
|
84
|
-
*
|
|
85
|
-
* `command` takes, by action name; `subcommands` adds commands of your
|
|
86
|
-
*
|
|
87
|
-
* server instead. A test runs it in memory, with no process:
|
|
88
|
-
* `Command.runWith(command, { version })(args)`.
|
|
97
|
+
* Project the implemented actions, every one or those `actions` selects, as subcommands of one
|
|
98
|
+
* aggregate command, each derived as `command` derives it and run in process. `commands` gives a
|
|
99
|
+
* subcommand the options `command` takes, by action name; `subcommands` adds commands of your
|
|
100
|
+
* own. `remote` calls a server instead. A test runs it in memory, with no process: `Command.runWith(command, { version })(args)`.
|
|
89
101
|
* Guide: `docs/ActionCli.md` in this package; in an application: `examples/assistant/src/commands.ts`.
|
|
102
|
+
*
|
|
103
|
+
* **Gotchas**
|
|
104
|
+
*
|
|
105
|
+
* - Services provided on it with `Command.provide` are also built when it runs with no subcommand, before it shows its help.
|
|
90
106
|
*/
|
|
91
107
|
function make(implementations, options) {
|
|
92
108
|
const apps = toList(implementations);
|
|
@@ -96,18 +112,22 @@ function make(implementations, options) {
|
|
|
96
112
|
}
|
|
97
113
|
function remote(binding, options) {
|
|
98
114
|
const listed = options.actions;
|
|
99
|
-
if (listed !== void 0) assertHeld(
|
|
115
|
+
if (listed !== void 0) assertHeld(notHeld.byBinding, listed, binding.actions);
|
|
100
116
|
const commands = checkedCommandsOf(options.commands, binding.actions);
|
|
101
117
|
return aggregateCommand(options.name, binding.actions.filter((action) => listed?.includes(action) ?? true), commands, options.subcommands ?? [], (action, syntax) => overHttp(binding, action, syntax, options.client));
|
|
102
118
|
}
|
|
103
119
|
/**
|
|
104
120
|
* A program whose stdout carries only its results, such as a CLI feeding scripts or an MCP
|
|
105
121
|
* subprocess, applied last, before `NodeRuntime.runMain`: the default logger of every layer
|
|
106
|
-
* provided within writes to stderr, and a failure `runMain` would report, such as a
|
|
107
|
-
* a failure of a layer the host provides, is reported
|
|
108
|
-
* own failure, which `Command.run` has printed, is not
|
|
109
|
-
* one `runMain` gives: the failure's
|
|
110
|
-
* interruption.
|
|
122
|
+
* provided within writes to stderr, and a failure `runMain` would report on stdout, such as a
|
|
123
|
+
* defect, a success that does not encode or a failure of a layer the host provides, is reported
|
|
124
|
+
* on stderr instead, once. A command's own failure, which `Command.run` has printed, is not
|
|
125
|
+
* reported again. The exit code is the one `runMain` gives: the failure's
|
|
126
|
+
* `Runtime.errorExitCode`, 1 by default, or 130 for an interruption.
|
|
127
|
+
*
|
|
128
|
+
* **Gotchas**
|
|
129
|
+
*
|
|
130
|
+
* - It moves the default logger only: a host layer's `Console` output, the global `console.log`, and a logger writing through `Console.log` such as `Logger.consoleJson`, still reach stdout; log JSON with `Logger.withConsoleError(Logger.formatJson)`.
|
|
111
131
|
*/
|
|
112
132
|
const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
|
|
113
133
|
if (Cause.hasInterruptsOnly(cause)) return Effect.interrupt;
|
|
@@ -118,5 +138,4 @@ const logToStderr = (self) => self.pipe(Effect.catchCause((cause) => {
|
|
|
118
138
|
});
|
|
119
139
|
return Effect.andThen(Runtime.getErrorReported(squashed) ? Effect.logError(cause) : Effect.void, Effect.die(reportedDefectKeepingCause));
|
|
120
140
|
}), Effect.provideService(Logger.LogToStderr, true));
|
|
121
|
-
//#endregion
|
|
122
141
|
export { command, logToStderr, make, remote, remoteCommand, toUserError };
|
package/dist/ActionHttp.d.mts
CHANGED
|
@@ -8,12 +8,16 @@ import { AnyHttp, Client, MethodError, Options } from "./http/client.mjs";
|
|
|
8
8
|
import { Context, Effect, FileSystem, Layer, Path, Schema, Types } from "effect";
|
|
9
9
|
import { Etag, HttpClient, HttpClientError, HttpPlatform, HttpRouter } from "effect/http";
|
|
10
10
|
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware } from "effect/http-api";
|
|
11
|
-
//#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<E = HttpClientError.HttpClientError, R = never> extends Options<E, R> {
|
|
14
13
|
/**
|
|
15
14
|
* What each call sends with. Omitted: the global `fetch`, looked up on every call, so one
|
|
16
15
|
* installed after the client is built, such as a test's stub, is used.
|
|
16
|
+
*
|
|
17
|
+
* **Gotchas**
|
|
18
|
+
*
|
|
19
|
+
* - A page on the API's own origin sends its cookies already; from another origin pass `(input, init) => fetch(input, { ...init, credentials: "include" })`, and the server's CORS must allow credentials, `HttpRouter.cors({ allowedOrigins, credentials: true })`.
|
|
20
|
+
* - A `fetch` of your own passes `init.signal` on, or interrupting a call no longer aborts its request: a deadline is `Effect.timeout` on the call.
|
|
17
21
|
*/
|
|
18
22
|
readonly fetch?: typeof globalThis.fetch | undefined;
|
|
19
23
|
}
|
|
@@ -30,14 +34,16 @@ type HttpLayer<H extends AnyHttp, Apps extends Served, M extends Middleware, A e
|
|
|
30
34
|
export interface LayerOptions<M extends Middleware = [], A extends Any = never> {
|
|
31
35
|
/**
|
|
32
36
|
* The actions it serves, among the binding's actions the implementations hold: `[GetUser]`.
|
|
33
|
-
* Defaults to every one of them.
|
|
34
|
-
* protected actions only.
|
|
37
|
+
* Defaults to every one of them.
|
|
35
38
|
*/
|
|
36
39
|
readonly actions?: ReadonlyArray<A> | undefined;
|
|
37
40
|
/**
|
|
38
41
|
* Native endpoint middleware every served route runs, the first innermost: inside the
|
|
39
|
-
* authentication of a protected route, outside its decoding
|
|
40
|
-
*
|
|
42
|
+
* authentication of a protected route, outside its decoding, so it also sees invalid input.
|
|
43
|
+
*
|
|
44
|
+
* **Gotchas**
|
|
45
|
+
*
|
|
46
|
+
* - A middleware requiring the identity needs a layer serving only protected actions: serve the public ones from a second layer without it.
|
|
41
47
|
*/
|
|
42
48
|
readonly middleware?: M;
|
|
43
49
|
}
|
|
@@ -46,9 +52,31 @@ type LayerRequest<App, A extends Any, M extends Middleware> = Exclude<Through<M,
|
|
|
46
52
|
}>] extends [never] ? Identity<Protected<Serving<App, A>>> : never)>;
|
|
47
53
|
/** Contract-level configuration shared by servers and clients. */
|
|
48
54
|
interface Options$1<E extends Errors = Errors> {
|
|
55
|
+
/**
|
|
56
|
+
* The mount path of every route: `/api` by default, `/` at the root. Split a large API, or
|
|
57
|
+
* actions sharing a name, into one binding per area, each with its own `prefix` and client;
|
|
58
|
+
* one host serves them all.
|
|
59
|
+
*/
|
|
49
60
|
readonly prefix?: `/${string}`;
|
|
50
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* What router middleware around the routes, or the layer's `middleware`, answers with, such as
|
|
63
|
+
* a quota's 429, which every endpoint declares: one schema, or a list.
|
|
64
|
+
*
|
|
65
|
+
* **Gotchas**
|
|
66
|
+
*
|
|
67
|
+
* - An error a handler fails with goes in its action's `error`, not here.
|
|
68
|
+
* - Router middleware answering with one encodes and sends it itself, `HttpServerResponse.schemaJson(QuotaExceeded)(error, { status: 429 })`, at the status `Action.Options.error` gives it: at any other a client fails with an `HttpClientError`.
|
|
69
|
+
*/
|
|
51
70
|
readonly error?: E | E[number];
|
|
71
|
+
/**
|
|
72
|
+
* The descriptor of the binding's protected actions, `Authentication.make(...)`: required
|
|
73
|
+
* when it holds one, and it documents and enforces their credentials as native endpoint
|
|
74
|
+
* security middleware on each of them.
|
|
75
|
+
*
|
|
76
|
+
* **Gotchas**
|
|
77
|
+
*
|
|
78
|
+
* - One descriptor per binding: actions of a second identity take a second binding with its own `prefix`, and a public action may be in both.
|
|
79
|
+
*/
|
|
52
80
|
readonly authentication?: Any$1;
|
|
53
81
|
}
|
|
54
82
|
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;
|
|
@@ -59,6 +87,7 @@ type Api<Actions extends ReadonlyArray<Any>, E extends Errors, D> = HttpApi.Http
|
|
|
59
87
|
* serves the same; `layer` serves it and `client` calls it.
|
|
60
88
|
*/
|
|
61
89
|
export interface Binding<Actions extends ReadonlyArray<Any>, E extends Errors = [], D extends Any$1 | undefined = undefined> {
|
|
90
|
+
/** Its protected actions' authentication descriptor, or `undefined` without one. */
|
|
62
91
|
readonly authentication: D;
|
|
63
92
|
/** The exact actions bound to this binding. */
|
|
64
93
|
readonly actions: Actions;
|
|
@@ -66,16 +95,26 @@ export interface Binding<Actions extends ReadonlyArray<Any>, E extends Errors =
|
|
|
66
95
|
readonly error: E;
|
|
67
96
|
/** Where its routes mount: `/api` by default, `/` at the root, without a trailing slash. */
|
|
68
97
|
readonly prefix: `/${string}`;
|
|
98
|
+
/**
|
|
99
|
+
* The native `HttpApi`, for `OpenApi.fromApi`, `HttpApiSwagger.layer`, `HttpApiScalar.layer`
|
|
100
|
+
* and Effect's own tools: it lists every action of the binding, served or not.
|
|
101
|
+
*
|
|
102
|
+
* **Gotchas**
|
|
103
|
+
*
|
|
104
|
+
* - Serve it with `layer`, never the native `HttpApiBuilder`: its group name is typed `string`, so `HttpApiBuilder.group(api, "api", …)` is refused as `Endpoint not handled: ${string}`, and given a `string`-typed name a protected action makes the host fail to build with `Service not found: effect-actions/Authentication/Security/…`.
|
|
105
|
+
* - A header that middleware reads is not in its document, whose operations list no `parameters`: rewrite it, `OpenApi.fromApi(Http.api.annotate(OpenApi.Transform, (document) => …))`, or make the value an input.
|
|
106
|
+
* - An OAuth protected resource is documented as a bearer scheme: Effect has no OAuth scheme.
|
|
107
|
+
* - Combined into one document, `HttpApi.make("app").addHttpApi(A.api).addHttpApi(B.api)`, a shared action name makes `OpenApi.fromApi` throw `Duplicate OpenAPI operationId`, and a shared prefix silently drops one binding's actions from the document and a native client: otherwise document each alone.
|
|
108
|
+
* - A binding holding no protected action documents no security scheme, so Swagger shows no Authorize button.
|
|
109
|
+
*/
|
|
69
110
|
readonly api: Api<Actions, E, D>;
|
|
70
111
|
}
|
|
71
112
|
/**
|
|
72
|
-
* Bind actions once, for servers and clients alike, each at `POST <prefix>/<action
|
|
73
|
-
* Every endpoint declares its action's errors, the binding's `error`, and
|
|
74
|
-
* `InvalidInput`, `Unauthenticated` and `Forbidden`, so clients decode each as a
|
|
75
|
-
* failure
|
|
76
|
-
*
|
|
77
|
-
* security middleware: the same descriptor documents their credentials and enforces them.
|
|
78
|
-
* Guide: `docs/ActionHttp.md` in this package; in an application: `examples/notes-service/src/routes.ts`.
|
|
113
|
+
* Bind actions once, for servers and clients alike, each at `POST <prefix>/<action>`, a
|
|
114
|
+
* `readOnly` one too. Every endpoint declares its action's errors, the binding's `error`, and
|
|
115
|
+
* the built-in `InvalidInput`, `Unauthenticated` and `Forbidden`, so clients decode each as a
|
|
116
|
+
* typed failure; a protected one also declares what its descriptor's verifier fails with.
|
|
117
|
+
* Guide: `docs/ActionHttp.md` in this package; in an application: `examples/notes-service/src/Api.ts`.
|
|
79
118
|
*/
|
|
80
119
|
export declare function make<const Actions extends ReadonlyArray<Any & {
|
|
81
120
|
readonly caller: typeof Anyone;
|
|
@@ -84,41 +123,45 @@ export declare function make<const Actions extends ReadonlyArray<Any>, const O e
|
|
|
84
123
|
export declare function make<const Actions extends ReadonlyArray<Any>>(actions: Actions & PublicOnly<Actions>): Binding<Actions, [], undefined>;
|
|
85
124
|
/**
|
|
86
125
|
* Serve the binding's actions among `implementations` in one layer, or those its `actions` lists,
|
|
87
|
-
* authenticating a protected action's request before decoding it, then running its
|
|
88
|
-
* `authorize` before each handler.
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
126
|
+
* authenticating a protected action's request before decoding it, then running its
|
|
127
|
+
* implementation's `authorize` before each handler. An implementation's actions the binding
|
|
128
|
+
* leaves out have no route here, one holding none of those served is not built, and a non-empty
|
|
129
|
+
* list holding none of the binding's actions is refused when `layer` is called. Several layers
|
|
130
|
+
* may serve one binding, each with its own `actions` and `middleware` covering its routes only.
|
|
131
|
+
* The layer fails as the builders do, needs at startup what they need, and per request what the
|
|
132
|
+
* authorization and handlers of the actions it serves need, until its `middleware` or middleware
|
|
133
|
+
* around it provides them, the identity of a protected action excepted, which its authentication
|
|
134
|
+
* provides; what is provided per request wins over what the layer was built with. A declared
|
|
135
|
+
* error answers with its JSON at the status `Action.Options.error` gives it, input that does not
|
|
136
|
+
* decode a 400 `InvalidInput`, and a refusal as `Action.Unauthenticated` and `Action.Forbidden`
|
|
137
|
+
* say.
|
|
138
|
+
*
|
|
139
|
+
* **Gotchas**
|
|
95
140
|
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* `
|
|
141
|
+
* - A success is its JSON as the body; an action without `success` answers an empty 200, no body and no content type, so a hand-written client must not parse it. One without `input` still takes the body `{}` typed as JSON: no body is a 400.
|
|
142
|
+
* - A body that is not JSON is a 400 `InvalidInput`; a request with no `Content-Type` a 415 `Unsupported content-type: none`, one with another type Effect's own 415; each after authentication on a protected route.
|
|
143
|
+
* - A defect, an undeclared failure, or a success or an error that does not encode answers an empty 500: the cause goes to the log and to the call's `ErrorReporter`s, never to the body.
|
|
144
|
+
* - A path no layer serves, or a method other than `POST` on a route, is a 404.
|
|
145
|
+
* - The 415 keeps from every handler what another site's page can send without a CORS preflight, such as a form post carrying the user's cookie; the server's CORS policy decides the rest.
|
|
146
|
+
* - Router middleware providing a request service `X` is declared `HttpRouter.middleware<{ provides: X }>()`: without the type argument it runs and the layer still owes `X`.
|
|
147
|
+
* - `HttpRouter.provideRequest(layer)` builds its layer once, at startup, so it never provides an identity or a tenant.
|
|
148
|
+
* - A request body is read whole, however large, unless the host provides `HttpServerRequest.MaxBodySize` to `HttpRouter.serve`, limiting every route that reads its body whole, MCP endpoints included; a body a route reads as a stream is not limited. On Node a request over it is an empty 413 from Effect 4.0.6, and before it gets no response, its connection closed; `HttpRouter.toWebHandler` ignores it, so limit bodies in the platform serving the handler.
|
|
149
|
+
* - An `ErrorReporter` the server runs with is given, as `Info`, a handler's declared failure and an input that does not decode, never a refusal or a refused credential: give an error class `override readonly [ErrorReporter.ignore] = true` to keep it out, and drop `Info` for a built-in error, which takes no mark.
|
|
104
150
|
*/
|
|
105
151
|
export declare function layer<const H extends AnyHttp, const Apps extends Served>(binding: H, implementations: Apps): HttpLayer<H, Apps, []>;
|
|
106
152
|
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]>>;
|
|
107
153
|
/**
|
|
108
154
|
* Effect's native `HttpApiClient` for a binding, one method per action taking the
|
|
109
|
-
* action's input directly: `client.greet({ name })
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* `
|
|
113
|
-
*
|
|
155
|
+
* action's input directly: `client.greet({ name })`, the argument omitted when `{}` is a valid
|
|
156
|
+
* input. Requires the native `HttpClient`, as `HttpApiClient.make` does; the native client
|
|
157
|
+
* itself stays available: `HttpApiClient.make(Http.api)`. Each call fails with its
|
|
158
|
+
* `MethodError`, an undeclared answer such as a 502 from a proxy or a defect's empty 500 as an
|
|
159
|
+
* `HttpClientError`, and with what `transformClient` adds.
|
|
114
160
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* input that does not encode fails with `InvalidInput`, and nothing is sent. A call also fails
|
|
120
|
-
* with what the client `transformClient` returns fails with, as it is, and requires what that
|
|
121
|
-
* client requires.
|
|
161
|
+
* **Gotchas**
|
|
162
|
+
*
|
|
163
|
+
* - An input that does not encode fails with `InvalidInput`, and nothing is sent; undeclared input fields, which TypeScript lets through on a wider value, are dropped as it encodes, since the server would refuse them.
|
|
164
|
+
* - It interprets no status and retries nothing: a failed write may have happened, and only a declared error says what the server did.
|
|
122
165
|
*/
|
|
123
166
|
export declare function client<const H extends AnyHttp>(binding: H, options?: Options): Effect.Effect<Client<H>, never, HttpClient.HttpClient>;
|
|
124
167
|
export declare function client<const H extends AnyHttp, E, R>(binding: H, options: Options<E, R>): Effect.Effect<Client<H, E, R>, never, HttpClient.HttpClient>;
|
|
@@ -127,11 +170,7 @@ export declare function client<const H extends AnyHttp, E, R>(binding: H, option
|
|
|
127
170
|
* a browser app: its methods are the same Effects, which require only what `transformClient`
|
|
128
171
|
* requires, so a call without one runs alone, as `Effect.runPromise(api.greet({ name }))`. The
|
|
129
172
|
* options are `client`'s, and `fetch`.
|
|
130
|
-
*
|
|
131
|
-
* Each call sends with `options.fetch`, or with the global `fetch` as it is when the call
|
|
132
|
-
* runs.
|
|
133
173
|
*/
|
|
134
174
|
export declare function fetchClient<const H extends AnyHttp>(binding: H, options?: FetchClientOptions): Client<H>;
|
|
135
175
|
export declare function fetchClient<const H extends AnyHttp, E, R>(binding: H, options: FetchClientOptions<E, R>): Client<H, E, R>;
|
|
136
|
-
//#endregion
|
|
137
176
|
export { type AnyHttp as Any, type Client, type Options as ClientOptions, type MethodError, Options$1 as Options };
|
package/dist/ActionHttp.mjs
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Anyone } from "./contract/caller.mjs";
|
|
2
|
+
import { declared } from "./contract/status.mjs";
|
|
2
3
|
import { bindingErrors, bindingOf, projectedErrors } from "./contract/declared.mjs";
|
|
3
4
|
import { acquire, provideHandlers, servingOf } from "./contract/implementation.mjs";
|
|
4
5
|
import { providerOf } from "./authentication/provider.mjs";
|
|
5
6
|
import { inputParseOptions } from "./contract/call.mjs";
|
|
6
|
-
import { declared } from "./contract/status.mjs";
|
|
7
7
|
import { methods } from "./http/client.mjs";
|
|
8
8
|
import { SchemaErrors, schemaErrors } from "./http/schema-errors.mjs";
|
|
9
9
|
import { Context, Effect, Layer, Scope } from "effect";
|
|
10
10
|
import { FetchHttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
11
11
|
import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApiMiddleware, HttpApiSchema, OpenApi } from "effect/http-api";
|
|
12
|
-
|
|
12
|
+
// src/http/ActionHttp.ts
|
|
13
13
|
const mountSegments = (prefix) => (prefix ?? "/api").split("/").filter((segment) => segment !== "");
|
|
14
14
|
const absolutePath = (segments) => `/${segments.join("/")}`;
|
|
15
15
|
const groupName = (mount) => mount.join("/") || "/";
|
|
@@ -21,7 +21,7 @@ const apiOf = (group, endpoints) => {
|
|
|
21
21
|
const endpointOf = (action, path, errors) => HttpApiEndpoint.post(action.name, path, {
|
|
22
22
|
payload: action.input.pipe(HttpApiSchema.asJson()),
|
|
23
23
|
success: action.success,
|
|
24
|
-
error: errors.
|
|
24
|
+
error: errors.map(declared)
|
|
25
25
|
});
|
|
26
26
|
const withinLayerMiddleware = (endpoint, middleware = []) => middleware.reduce((inner, key) => inner.middleware(key), endpoint);
|
|
27
27
|
function make(actions, options = {}) {
|
|
@@ -77,5 +77,4 @@ function client(binding, options) {
|
|
|
77
77
|
function fetchClient(binding, { fetch, ...options } = {}) {
|
|
78
78
|
return methods(binding, options).pipe(Effect.provide(FetchHttpClient.layer), Effect.provideService(FetchHttpClient.Fetch, fetch ?? ((input, init) => globalThis.fetch(input, init))), Effect.runSync);
|
|
79
79
|
}
|
|
80
|
-
//#endregion
|
|
81
80
|
export { client, fetchClient, layer, make };
|