@gjermundgaraba/effect-actions 0.19.0 → 0.21.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 +19 -41
- package/dist/Action.d.mts +140 -48
- package/dist/Action.mjs +88 -44
- package/dist/ActionCli.d.mts +70 -35
- package/dist/ActionCli.mjs +53 -34
- package/dist/ActionHttp.d.mts +70 -38
- package/dist/ActionHttp.mjs +76 -25
- package/dist/ActionMcp.d.mts +56 -24
- package/dist/ActionMcp.mjs +104 -61
- package/dist/ActionRpc.d.mts +222 -57
- package/dist/ActionRpc.mjs +301 -49
- package/dist/ActionToolkit.d.mts +22 -7
- package/dist/ActionToolkit.mjs +37 -14
- package/dist/Authentication.d.mts +42 -12
- package/dist/Authentication.mjs +38 -32
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.d.mts +1 -3
- package/dist/Testing.mjs +2 -3
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -12
- package/dist/authentication/provider.mjs +2 -3
- package/dist/authentication/refusal.mjs +3 -4
- package/dist/cli/commands.d.mts +9 -7
- package/dist/cli/commands.mjs +85 -35
- package/dist/contract/ast.mjs +5 -3
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -8
- package/dist/contract/call.mjs +49 -8
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +12 -3
- package/dist/contract/caller.mjs +23 -5
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.d.mts +0 -2
- package/dist/contract/declared.mjs +26 -11
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -5
- package/dist/contract/errors.mjs +2 -3
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hints.mjs +1 -2
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -51
- package/dist/contract/implementation.mjs +94 -60
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/media.d.mts +1 -3
- package/dist/contract/media.mjs +1 -2
- package/dist/contract/names.mjs +15 -6
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +15 -7
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -14
- package/dist/http/client.mjs +52 -14
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +2 -3
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -99
- package/dist/mcp/client.mjs +60 -134
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +40 -15
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +14 -7
- package/dist/testing/memory.d.mts +1 -3
- package/dist/testing/memory.mjs +4 -5
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +57 -61
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -53
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +25 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +130 -33
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +19 -17
package/docs/Action.md
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
# Action
|
|
2
2
|
|
|
3
|
-
`Action.make` defines a contract: what an action takes and answers, whether it changes anything,
|
|
4
|
-
and who may call it. `Action.implement` binds contracts to handlers, behind an authorizer. Every
|
|
5
|
-
surface takes the implementation it returns, or a list of them, and `Action.client` calls one in
|
|
6
|
-
process.
|
|
3
|
+
`Action.make` defines a contract: what an action takes and answers, whether it changes anything, and who may call it. `Action.implement` binds contracts to handlers, behind an authorizer. Every surface takes the implementation it returns, or a list of them, and `Action.client` calls one in process.
|
|
7
4
|
|
|
8
|
-
A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search
|
|
9
|
-
[Action-failures.md](Action-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`:
|
|
10
|
-
each declaration's JSDoc carries its rules and gotchas, and this page only what an example
|
|
11
|
-
cannot show.
|
|
5
|
+
A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [Action-failures.md](Action-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show. An action reporting progress or answering a stream is on [Action-streams.md](Action-streams.md); what happens apart from a call, which a subscriber watches, is an event type, on [Event.md](Event.md).
|
|
12
6
|
|
|
13
7
|
## Define a contract
|
|
14
8
|
|
|
15
|
-
One `Action.make` per action, in the domain's contract file. Export the domain's actions as one
|
|
16
|
-
`as const` list ([notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts)):
|
|
17
|
-
`implement` and every surface take lists.
|
|
9
|
+
One `Action.make` per action, in the domain's contract file. Export the domain's actions as one `as const` list ([notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts)): `implement` and every surface take lists.
|
|
18
10
|
|
|
19
11
|
```ts example=quickstart/src/greeting/Greeting.ts
|
|
20
|
-
import { Schema } from "effect";
|
|
21
12
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
13
|
+
import { Schema } from "effect";
|
|
22
14
|
|
|
23
15
|
// A contract: what the action takes and answers, whether it changes anything, and who may
|
|
24
16
|
// call it. It imports no server code, so any client can import it, a browser page included.
|
|
@@ -31,48 +23,26 @@ export const Greet = Action.make("greet", {
|
|
|
31
23
|
});
|
|
32
24
|
```
|
|
33
25
|
|
|
34
|
-
[notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts) adds declared
|
|
35
|
-
errors with their statuses, described fields and the list;
|
|
36
|
-
[kitchen-sink/src/issues/Issues.ts](../examples/kitchen-sink/src/issues/Issues.ts) uses every
|
|
37
|
-
option. The rules of each option, `input`'s optional fields and `error`'s tags included, are on
|
|
38
|
-
`Action.Options`; where an action must be listed to be served, and matching by object identity,
|
|
39
|
-
are on `Action.make`. The list each surface takes lives in one file beside the contracts
|
|
40
|
-
([kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts)).
|
|
26
|
+
[notes-service/src/notes/Notes.ts](../examples/notes-service/src/notes/Notes.ts) adds declared errors with their statuses, described fields and the list; [kitchen-sink/src/issues/Issues.ts](../examples/kitchen-sink/src/issues/Issues.ts) uses every option. The rules of each option, `input`'s optional fields and `error`'s tags included, are on `Action.Options`; where an action must be listed to be served, and matching by object identity, are on `Action.make`. The list each surface takes lives in one file beside the contracts ([kitchen-sink/src/Actions.ts](../examples/kitchen-sink/src/Actions.ts)).
|
|
41
27
|
|
|
42
|
-
- **List an action as a tool only where a model may hold what it takes and returns.** A tool
|
|
43
|
-
call passes both through the model's context and records its arguments on the current span,
|
|
44
|
-
a `Schema.Redacted` value included.
|
|
28
|
+
- **List an action as a tool only where a model may hold what it takes and returns.** A tool call passes both through the model's context, and `ActionToolkit`, through Effect's toolkit, records its arguments on the current span, a `Schema.Redacted` value included.
|
|
45
29
|
|
|
46
30
|
## Say who may call an action
|
|
47
31
|
|
|
48
|
-
`caller: Action.Anyone` makes a public action; an identity service, `caller: CurrentUser`, a
|
|
49
|
-
protected one, whose identity a remote surface gets by authenticating the request
|
|
50
|
-
([Authentication.md](Authentication.md#declare-how-callers-sign-in)) and a local surface, which
|
|
51
|
-
has no one to authenticate, from the host ([ActionToolkit.md](ActionToolkit.md#name-the-caller),
|
|
52
|
-
[ActionCli.md](ActionCli.md#name-the-caller), [ActionMcp.md](ActionMcp.md#serve-tools-over-stdio)).
|
|
32
|
+
`caller: Action.Anyone` makes a public action; an identity service, `caller: CurrentUser`, a protected one, whose identity a remote surface gets by authenticating the request ([Authentication.md](Authentication.md#declare-how-callers-sign-in)) and a local surface, which has no one to authenticate, from the host ([ActionToolkit.md](ActionToolkit.md#name-the-caller), [ActionCli.md](ActionCli.md#name-the-caller), [ActionMcp.md](ActionMcp.md#serve-tools-over-stdio)).
|
|
53
33
|
|
|
54
|
-
`CurrentUser` is declared in
|
|
55
|
-
[notes-service/src/identity/Identity.ts](../examples/notes-service/src/identity/Identity.ts), and
|
|
56
|
-
a public action in [notes-service/src/status/Status.ts](../examples/notes-service/src/status/Status.ts).
|
|
57
|
-
The rules of an identity, never provided at startup, are on `Action.Options.caller`; a test
|
|
58
|
-
sends each caller's credential instead
|
|
59
|
-
([Testing.md](Testing.md#test-as-a-signed-in-caller)).
|
|
34
|
+
`CurrentUser` is declared in [notes-service/src/identity/Identity.ts](../examples/notes-service/src/identity/Identity.ts), and a public action in [notes-service/src/status/Status.ts](../examples/notes-service/src/status/Status.ts). The rules of an identity, never provided at startup, are on `Action.Options.caller`; a test sends each caller's credential instead ([Testing.md](Testing.md#test-as-a-signed-in-caller)).
|
|
60
35
|
|
|
61
|
-
- **One protected contract serves callers with a token over HTTP and a local host without
|
|
62
|
-
one**, never a public copy of it: the host provides the caller around `runStdio`
|
|
63
|
-
([notes-service/src/stdio.ts](../examples/notes-service/src/stdio.ts)); a second audience is
|
|
64
|
-
`Operator` in [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts).
|
|
36
|
+
- **One protected contract serves callers with a token over HTTP and a local host without one**, never a public copy of it: the host provides the caller around `runStdio` ([notes-service/src/stdio.ts](../examples/notes-service/src/stdio.ts)); a second audience is `Operator` in [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts).
|
|
65
37
|
|
|
66
38
|
## Implement actions
|
|
67
39
|
|
|
68
|
-
`Action.implement` binds a handler to one action, or a record of handlers keyed by name to a
|
|
69
|
-
list. Either may instead be a builder: an Effect returning the handler or the record, whose
|
|
70
|
-
services are startup services. What a handler yields per call, the caller first, is a
|
|
71
|
-
request-time requirement.
|
|
40
|
+
`Action.implement` binds a handler to one action, or a record of handlers keyed by name to a list. Either may instead be a builder: an Effect returning the handler or the record, whose services are startup services. What a handler yields per call, the caller first, is a request-time requirement.
|
|
72
41
|
|
|
73
42
|
```ts example=notes-service/src/notes/handlers.ts
|
|
74
|
-
import { Effect } from "effect";
|
|
75
43
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
44
|
+
import { Effect } from "effect";
|
|
45
|
+
|
|
76
46
|
import { authorize } from "../identity/authorize.js";
|
|
77
47
|
import { CurrentUser } from "../identity/Identity.js";
|
|
78
48
|
import { Notes } from "./Notes.js";
|
|
@@ -95,47 +65,44 @@ export const notes = Action.implement(
|
|
|
95
65
|
|
|
96
66
|
return { notes: owned.map(({ id, title }) => ({ id, title })) };
|
|
97
67
|
}),
|
|
98
|
-
getNote: ({ id }) =>
|
|
68
|
+
getNote: ({ id }) =>
|
|
69
|
+
Effect.flatMap(CurrentUser, (user) => store.get(user.id, id)),
|
|
99
70
|
createNote: ({ requestId, title, body }) =>
|
|
100
|
-
Effect.flatMap(CurrentUser, (user) =>
|
|
71
|
+
Effect.flatMap(CurrentUser, (user) =>
|
|
72
|
+
store.create(user.id, title, body, requestId)
|
|
73
|
+
),
|
|
101
74
|
updateNote: ({ id, body, revision }) =>
|
|
102
|
-
Effect.flatMap(CurrentUser, (user) =>
|
|
103
|
-
|
|
104
|
-
|
|
75
|
+
Effect.flatMap(CurrentUser, (user) =>
|
|
76
|
+
store.update(user.id, id, body, revision)
|
|
77
|
+
),
|
|
78
|
+
deleteNote: ({ id }) =>
|
|
79
|
+
Effect.flatMap(CurrentUser, (user) => store.remove(user.id, id)),
|
|
80
|
+
importNotes: ({ notes: imported }) =>
|
|
105
81
|
Effect.map(
|
|
106
|
-
Effect.flatMap(CurrentUser, (user) =>
|
|
107
|
-
|
|
82
|
+
Effect.flatMap(CurrentUser, (user) =>
|
|
83
|
+
store.createAll(user.id, imported)
|
|
84
|
+
),
|
|
85
|
+
(created) => ({ notes: created })
|
|
108
86
|
),
|
|
109
87
|
};
|
|
110
88
|
}),
|
|
111
|
-
{ authorize }
|
|
89
|
+
{ authorize }
|
|
112
90
|
);
|
|
113
91
|
```
|
|
114
92
|
|
|
115
|
-
A plain handler, no builder, is
|
|
116
|
-
[quickstart/src/greeting/handlers.ts](../examples/quickstart/src/greeting/handlers.ts). The
|
|
117
|
-
builder's rules, once per layer graph, a scope per call, passing the builder to `implement`
|
|
118
|
-
itself, calling through `Action.client` and never the function you passed, are on
|
|
119
|
-
`Action.implement`.
|
|
93
|
+
A plain handler, no builder, is [quickstart/src/greeting/handlers.ts](../examples/quickstart/src/greeting/handlers.ts). The builder's rules, once per layer graph, a scope per call, passing the builder to `implement` itself, calling through `Action.client` and never the function you passed, are on `Action.implement`.
|
|
120
94
|
|
|
121
|
-
- **One implementation may hold public and protected actions**, and actions only some
|
|
122
|
-
|
|
123
|
-
- **Leave a handler's `Effect.fn` unnamed** unless you need handler-only timing. The span named
|
|
124
|
-
after the action covers authorization, the handler and call cleanup, carrying `action.name`
|
|
125
|
-
and `action.read_only` on it and its logs. Credential verification and input decoding run
|
|
126
|
-
before it, so a request they reject has no action span; response encoding runs after it, so
|
|
127
|
-
it can fail after a successful one.
|
|
95
|
+
- **One implementation may hold public and protected actions**, and actions only some surfaces serve: group by domain.
|
|
96
|
+
- **Leave a handler's `Effect.fn` unnamed** unless you need handler-only timing. The span named after the action covers authorization, the handler and call cleanup, carrying `action.name` and `action.read_only` on it and its logs. Credential verification and input decoding run before it, so a request they reject has no action span; response encoding runs after it, so it can fail after a successful one.
|
|
128
97
|
|
|
129
98
|
## Authorize calls
|
|
130
99
|
|
|
131
|
-
An implementation holding a protected action states `{ authorize }`: a function of the called
|
|
132
|
-
action that succeeds, or fails with a refusal; no surface has a policy of its own. An
|
|
133
|
-
implementation of public actions alone takes none
|
|
134
|
-
([notes-service/src/status/handlers.ts](../examples/notes-service/src/status/handlers.ts)).
|
|
100
|
+
An implementation holding a protected action states `{ authorize }`: a function of the called action that succeeds, or fails with a refusal; no surface has a policy of its own. An implementation of public actions alone takes none ([notes-service/src/status/handlers.ts](../examples/notes-service/src/status/handlers.ts)).
|
|
135
101
|
|
|
136
102
|
```ts example=notes-service/src/identity/authorize.ts
|
|
137
|
-
import { Effect } from "effect";
|
|
138
103
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
104
|
+
import { Effect } from "effect";
|
|
105
|
+
|
|
139
106
|
import { CurrentUser, scopes } from "./Identity.js";
|
|
140
107
|
|
|
141
108
|
// The one rule, read from each contract: a read needs the read scope, anything else the write
|
|
@@ -147,73 +114,40 @@ export const authorize = Effect.fn("authorize")(function* (action: Action.Any) {
|
|
|
147
114
|
const user = yield* CurrentUser;
|
|
148
115
|
|
|
149
116
|
if (!user.scopes.includes(scope)) {
|
|
150
|
-
return yield* new Action.Forbidden({
|
|
117
|
+
return yield* new Action.Forbidden({
|
|
118
|
+
message: `Requires ${scope}.`,
|
|
119
|
+
scopes: [scope],
|
|
120
|
+
});
|
|
151
121
|
}
|
|
152
122
|
});
|
|
153
123
|
```
|
|
154
124
|
|
|
155
|
-
Actions under another rule go in another `Action.implement` with its own `authorize`, or
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
[
|
|
160
|
-
rule, and that a limit is the handler's, are on `Action.implement`.
|
|
161
|
-
|
|
162
|
-
- **A caller the authorizer would refuse can still receive an `InvalidInput` describing the
|
|
163
|
-
input's schema**, since input is decoded before the authorizer runs. To refuse or limit before
|
|
164
|
-
decoding, use the layer's `middleware`
|
|
165
|
-
([ActionHttp.md](ActionHttp.md#split-a-binding-across-layers-and-add-middleware),
|
|
166
|
-
[ActionRpc.md](ActionRpc.md#serve-the-rpcs)).
|
|
167
|
-
- **A limiter is a service the builder yields, apart from the store**
|
|
168
|
-
([kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts),
|
|
169
|
-
[kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts)): state kept in a builder
|
|
170
|
-
lasts only within one layer graph.
|
|
171
|
-
- **A check on one record belongs in the handler's data access**
|
|
172
|
-
([notes-service/src/notes/store.ts](../examples/notes-service/src/notes/store.ts)). So does a
|
|
173
|
-
rule across callers, such as uniqueness.
|
|
125
|
+
Actions under another rule go in another `Action.implement` with its own `authorize`, or `Action.allowAll`, not a branch on `action.name`. What a `Forbidden` with and without `scopes` answers on each surface is on `Forbidden`. Reading a store once, by an `authorize` that is an Effect returning the function, is [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts); that rule, and that a limit is the handler's, are on `Action.implement`.
|
|
126
|
+
|
|
127
|
+
- **A caller the authorizer would refuse can still receive an `InvalidInput` describing the input's schema**, since input is decoded before the authorizer runs. To refuse or limit before decoding, use the layer's `middleware` ([ActionHttp.md](ActionHttp.md#split-a-binding-across-layers-and-add-middleware), [ActionRpc.md](ActionRpc.md#serve-the-rpcs)).
|
|
128
|
+
- **A limiter is a service the builder yields, apart from the store** ([kitchen-sink/src/issues/write-limit.ts](../examples/kitchen-sink/src/issues/write-limit.ts), [kitchen-sink/src/memory.ts](../examples/kitchen-sink/src/memory.ts)): state kept in a builder lasts only within one layer graph.
|
|
129
|
+
- **A check on one record belongs in the handler's data access** ([notes-service/src/notes/store.ts](../examples/notes-service/src/notes/store.ts)). So does a rule across callers, such as uniqueness.
|
|
174
130
|
|
|
175
131
|
## Fail with declared and built-in errors
|
|
176
132
|
|
|
177
|
-
A handler fails with the errors its action lists in `error`, and with the three built-in
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
on** ([mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts)).
|
|
187
|
-
As defects, a model reads only a generic text.
|
|
188
|
-
- **Internal failures are never sent** over HTTP, MCP and RPC: what reaches the log and the
|
|
189
|
-
call's `ErrorReporter`s is on `ActionHttp.layer`, `ActionMcp.layerHttp` and `ActionRpc.layer`;
|
|
190
|
-
a Toolkit and a CLI make a defect the call's.
|
|
133
|
+
A handler fails with the errors its action lists in `error`, and with the three built-in errors, `InvalidInput`, `Unauthenticated` and `Forbidden`. What a refusal answers on each surface is on `Action.Unauthenticated` and `Action.Forbidden`; what a declared error and bad input answer, on each surface's `layer`, `layerHttp`, `runStdio`, `make` or `command`; when to fail with `InvalidInput` yourself, on that class, as [kitchen-sink/src/attachments/handlers.ts](../examples/kitchen-sink/src/attachments/handlers.ts) does.
|
|
134
|
+
|
|
135
|
+
- **Add the errors a module adds to every action it serves, a proxy's or a store's, to contracts written elsewhere with `Action.withErrors`**, `pipe(action, Action.withErrors([Unavailable]))`, rather than restating each contract. It returns a new contract, which the module then implements and lists; its rules are on `Action.withErrors`.
|
|
136
|
+
- **Map a client's `HttpClientError` and `SchemaError` to one declared error a caller can act on** ([mcp-tools/src/platform/print-farm.ts](../examples/mcp-tools/src/platform/print-farm.ts)). As defects, a model reads only a generic text.
|
|
137
|
+
- **Internal failures are never sent** over HTTP, MCP and RPC: what reaches the log and the call's `ErrorReporter`s is on `ActionHttp.layer`, `ActionMcp.layerHttp` and `ActionRpc.layer`; a Toolkit and a CLI make a defect the call's.
|
|
138
|
+
|
|
139
|
+
## Answer a retried call once
|
|
140
|
+
|
|
141
|
+
A write a caller may send again, after a dropped connection or a timeout, names a string field of its input as its `idempotencyKey`. The caller makes the key at random for the call and sends it again on each retry: a repeat with the same input is answered as the first call was, its handler run once, on every surface, since the key travels with the input. An implementation of such an action requires `Action.Idempotency` at startup, where the answers are kept: provide `Action.layerIdempotency` around the surfaces, as `Event.layerMemory` is provided; in a Durable Object, the Cloudflare package's `ActionDurableObject.make` provides it. What is kept, whose a key is, and what a repeat with another input answers, are on the option.
|
|
191
142
|
|
|
192
143
|
## Call actions in process
|
|
193
144
|
|
|
194
|
-
`Action.client(implementations)` gives the methods `ActionHttp.client` gives, for a job, an
|
|
195
|
-
agent or a test, without a transport. It is acquired where builders live:
|
|
196
|
-
[kitchen-sink/src/agent.ts](../examples/kitchen-sink/src/agent.ts) in a scoped program,
|
|
197
|
-
[notes-service/test/notes/in-process.test.ts](../examples/notes-service/test/notes/in-process.test.ts)
|
|
198
|
-
under `it.effect`. The rules are on `Action.client`; testing with it is
|
|
199
|
-
[Testing.md](Testing.md#test-an-implementation-in-process).
|
|
145
|
+
`Action.client(implementations)` gives the methods `ActionHttp.client` gives, for a job, an agent or a test, without a transport. It is acquired where builders live: [kitchen-sink/src/agent.ts](../examples/kitchen-sink/src/agent.ts) in a scoped program, [notes-service/test/notes/in-process.test.ts](../examples/notes-service/test/notes/in-process.test.ts) under `it.effect`. The rules are on `Action.client`; testing with it is [Testing.md](Testing.md#test-an-implementation-in-process).
|
|
200
146
|
|
|
201
147
|
## Share one build across servers
|
|
202
148
|
|
|
203
|
-
Inside one `HttpRouter.serve` or one `Testing.layer`, a builder runs once, however many surfaces
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- **With one server, put everything the process runs inside the layer it serves**, a job or an
|
|
209
|
-
agent beside the routes included; why a job merged beside `HttpRouter.serve` is not enough is
|
|
210
|
-
on `Action.layer`.
|
|
211
|
-
- **Otherwise, provide `Action.layer(implementations)` and the services they share above the
|
|
212
|
-
server and its siblings**: a second server, a program beside it, several `Testing.layer`s.
|
|
213
|
-
[kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts) does it for two ports with
|
|
214
|
-
the `builders` layer of [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts),
|
|
215
|
-
and [kitchen-sink/test/routes.test.ts](../examples/kitchen-sink/test/routes.test.ts) with two
|
|
216
|
-
`Testing.layer`s, asserting one run of a builder both ports serve.
|
|
217
|
-
- **Inside one layer graph, a builder runs with the services of whichever surface builds it
|
|
218
|
-
first**, and services provided around one surface reach the others: provide startup services
|
|
219
|
-
once, for every surface. Where two must differ is on `Action.layer`.
|
|
149
|
+
Inside one `HttpRouter.serve` or one `Testing.layer`, a builder runs once, however many surfaces serve it, a toolkit's `layer` provided to the routes included. Each `HttpRouter.serve` is a layer graph of its own, the layers Effect builds together, each once, which shares nothing it builds with another graph: a builder's run, a built authorizer, or an in-memory store of yours.
|
|
150
|
+
|
|
151
|
+
- **With one server, put everything the process runs inside the layer it serves**, a job or an agent beside the routes included; why a job merged beside `HttpRouter.serve` is not enough is on `Action.layer`.
|
|
152
|
+
- **Otherwise, provide `Action.layer(implementations)` and the services they share above the server and its siblings**: a second server, a program beside it, several `Testing.layer`s. [kitchen-sink/src/server.ts](../examples/kitchen-sink/src/server.ts) does it for two ports with the `builders` layer of [kitchen-sink/src/routes.ts](../examples/kitchen-sink/src/routes.ts), and [kitchen-sink/test/routes.test.ts](../examples/kitchen-sink/test/routes.test.ts) with two `Testing.layer`s, asserting one run of a builder both ports serve.
|
|
153
|
+
- **Inside one layer graph, a builder runs with the services of whichever surface builds it first**, and services provided around one surface reach the others: provide startup services once, for every surface. Where two must differ is on `Action.layer`.
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# ActionCli failure modes
|
|
2
2
|
|
|
3
|
-
What you see when something is wrong, and the fix. Search for a fragment of the message.
|
|
4
|
-
The tasks are in [ActionCli.md](ActionCli.md).
|
|
3
|
+
What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [ActionCli.md](ActionCli.md).
|
|
5
4
|
|
|
6
5
|
## Flags and input
|
|
7
6
|
|
package/docs/ActionCli.md
CHANGED
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
# ActionCli
|
|
2
2
|
|
|
3
|
-
Native Effect CLI commands for actions, with flags derived from each action's input. `make` and
|
|
4
|
-
`command` take implementations and run the handler in the command's own process, behind the same
|
|
5
|
-
`authorize`: for a local tool, or an operator console that has the services itself. `remote` and
|
|
6
|
-
`remoteCommand` take an HTTP binding and call a server, running nothing locally: for users of a
|
|
7
|
-
service you already serve. `make` and `remote` derive one command per action; which actions a
|
|
8
|
-
surface serves is on `Action.make`.
|
|
3
|
+
Native Effect CLI commands for actions, with flags derived from each action's input. `make` and `command` take implementations and run the handler in the command's own process, behind the same `authorize`: for a local tool, or an operator console that has the services itself. `remote` and `remoteCommand` take an HTTP binding and call a server, running nothing locally: for users of a service you already serve. `make` and `remote` derive one command per action; which actions a surface serves is on `Action.make`.
|
|
9
4
|
|
|
10
5
|
A message the library throws says its fix; for a compiler diagnostic or Effect's own error, search [ActionCli-failures.md](ActionCli-failures.md). For one symbol, `grep -rn <symbol> node_modules/@gjermundgaraba/effect-actions/dist/`: each declaration's JSDoc carries its rules and gotchas, and this page only what an example cannot show.
|
|
11
6
|
|
|
12
7
|
## Run actions as local commands
|
|
13
8
|
|
|
14
|
-
Pass implementations to `make`; the host provides the services and the caller on the command,
|
|
15
|
-
and runs it with Effect's own `Command.run`:
|
|
9
|
+
Pass implementations to `make`; the host provides the services and the caller on the command, and runs it with Effect's own `Command.run`:
|
|
16
10
|
|
|
17
11
|
```ts example=assistant/src/cli.ts
|
|
18
12
|
import { userInfo } from "node:os";
|
|
13
|
+
|
|
19
14
|
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
15
|
+
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
20
16
|
import { Effect, Layer } from "effect";
|
|
21
17
|
import { Command } from "effect/cli";
|
|
22
|
-
|
|
18
|
+
|
|
23
19
|
import { commands } from "./commands.js";
|
|
24
|
-
import { CurrentCaller
|
|
20
|
+
import { CurrentCaller } from "./identity/Identity.js";
|
|
21
|
+
import type { Operator } from "./identity/Identity.js";
|
|
25
22
|
import { Permissions } from "./identity/permissions.js";
|
|
26
23
|
import { tasksFile } from "./platform/tasks-file.js";
|
|
27
24
|
|
|
@@ -33,7 +30,7 @@ const operator: Operator = { kind: "operator", id: userInfo().username };
|
|
|
33
30
|
|
|
34
31
|
const assistant = commands.pipe(
|
|
35
32
|
Command.provide(Layer.mergeAll(tasksFile, Permissions.layerMemory)),
|
|
36
|
-
Command.provideSync(CurrentCaller, operator)
|
|
33
|
+
Command.provideSync(CurrentCaller, operator)
|
|
37
34
|
);
|
|
38
35
|
|
|
39
36
|
// A command's defect, such as a tasks file that does not decode, `runMain` reports on stdout.
|
|
@@ -42,81 +39,90 @@ const assistant = commands.pipe(
|
|
|
42
39
|
Command.run(assistant, { version: "1.0.0" }).pipe(
|
|
43
40
|
Effect.provide(NodeServices.layer),
|
|
44
41
|
ActionCli.logToStderr,
|
|
45
|
-
NodeRuntime.runMain
|
|
42
|
+
NodeRuntime.runMain()
|
|
46
43
|
);
|
|
47
44
|
```
|
|
48
45
|
|
|
49
|
-
The aggregate is [assistant/src/commands.ts](../examples/assistant/src/commands.ts), and
|
|
50
|
-
[kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts) an operator console. For one
|
|
51
|
-
action, or a tree of your own under native `Command.withSubcommands`, use
|
|
52
|
-
`command(implementations, action, options?)`. Where services go, what each run builds and what a
|
|
53
|
-
command prints on which stream are on `ActionCli.command`; what `runMain` reports, a command's
|
|
54
|
-
defect included, on `ActionCli.logToStderr`; matching a failure after `Command.run`, on
|
|
55
|
-
`ActionCli.UserError`.
|
|
46
|
+
The aggregate is [assistant/src/commands.ts](../examples/assistant/src/commands.ts), and [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts) an operator console. For one action, or a tree of your own under native `Command.withSubcommands`, use `command(implementations, action, options?)`. Where services go, what each run builds and what a command prints on which stream are on `ActionCli.command`; what `runMain` reports, a command's defect included, on `ActionCli.logToStderr`; matching a failure after `Command.run`, on `ActionCli.UserError`.
|
|
56
47
|
|
|
57
|
-
- **Test commands without a process** with `Command.runWith`, `TestConsole` and
|
|
58
|
-
`Stdio.layerTest`: [assistant/test/commands.test.ts](../examples/assistant/test/commands.test.ts).
|
|
48
|
+
- **Test commands without a process** with `Command.runWith`, `TestConsole` and `Stdio.layerTest`: [assistant/test/commands.test.ts](../examples/assistant/test/commands.test.ts).
|
|
59
49
|
|
|
60
50
|
## Call a server from commands
|
|
61
51
|
|
|
62
|
-
Pass the HTTP binding to `remote`. The program imports the binding alone, never a handler. The
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
52
|
+
Pass the HTTP binding to `remote`. The program imports the binding alone, never a handler. The connection is the `client` option, an `ActionHttp.client` ([ActionHttp.md](ActionHttp.md#call-the-api-from-effect)), its rules on `ActionCli.Calling`'s `client`, shared by `remote` and `remoteCommand`: [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts) requires a token, mapping its absence to an error that says what to do, and [kitchen-sink/src/cli.ts](../examples/kitchen-sink/src/cli.ts) adds a tenant header, reads an optional token for a public command. The host provides `HttpClient` (`NodeHttpClient.layerUndici`) in [notes-service/src/cli.ts](../examples/notes-service/src/cli.ts).
|
|
53
|
+
|
|
54
|
+
- **Split one binding into groups of commands**, one `remote` per group with its own `actions`; what each group's `client` needs is on `ActionCli.Calling`'s `client`.
|
|
55
|
+
- **Test a remote command the same way** as a local one, with `Testing.layer(routes)` provided as its `HttpClient` ([Testing.md](Testing.md#test-routes-in-memory)). Export the command from a module apart from the entrypoint that runs it ([notes-service/test/commands.test.ts](../examples/notes-service/test/commands.test.ts)).
|
|
56
|
+
|
|
57
|
+
## Watch events
|
|
58
|
+
|
|
59
|
+
`make` and `remote` add `watch <event>` for each [event type](Event.md) among the implementations or in the binding, or each one `actions` lists where it is given. Experimental, as [events](Event.md) are.
|
|
60
|
+
|
|
61
|
+
```ts example=build-events/src/commands.ts
|
|
62
|
+
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
63
|
+
import * as ActionHttp from "@gjermundgaraba/effect-actions/ActionHttp";
|
|
64
|
+
import { Config, Effect } from "effect";
|
|
65
|
+
import { HttpClient, HttpClientRequest } from "effect/http";
|
|
66
|
+
|
|
67
|
+
import { Http } from "./Api.js";
|
|
68
|
+
|
|
69
|
+
// The binding's client, connected to where the server is and as which member, both read from
|
|
70
|
+
// config when a command runs.
|
|
71
|
+
const api = Effect.gen(function* () {
|
|
72
|
+
const baseUrl = yield* Config.String("BUILDS_URL").pipe(
|
|
73
|
+
Config.withDefault("http://127.0.0.1:3000")
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const token = yield* Config.Redacted("BUILDS_TOKEN");
|
|
77
|
+
|
|
78
|
+
return yield* ActionHttp.client(Http, {
|
|
79
|
+
baseUrl,
|
|
80
|
+
transformClient: HttpClient.mapRequest(
|
|
81
|
+
HttpClientRequest.bearerToken(token)
|
|
82
|
+
),
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// A command per action, and `watch <event>` per event type: `builds watch build.finished
|
|
87
|
+
// --pipeline api` holds the event's stream open and prints each occurrence as a line of JSON,
|
|
88
|
+
// its `cursor` included, until it is stopped. `--cursor <cursor>` resumes after that line;
|
|
89
|
+
// omitted, the watch starts from now. The lines pipe into `jq` or a script as they come.
|
|
90
|
+
// `builds start-build --pipeline api` prints each step as a line of JSON on stderr as it
|
|
91
|
+
// starts, and the build on stdout, which a pipe reads alone. `builds tail-log --build-id
|
|
92
|
+
// build-1` prints each line of its log as a line of JSON as it arrives.
|
|
93
|
+
export const commands = ActionCli.remote(Http, { name: "builds", client: api });
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
[build-events/test/commands.test.ts](../examples/build-events/test/commands.test.ts) runs a watch and stops it. What a watch prints and fails with is on `make` and `remote`.
|
|
79
97
|
|
|
80
98
|
## Shape flags, input and output
|
|
81
99
|
|
|
82
100
|
### Flags from the input
|
|
83
101
|
|
|
84
|
-
A struct or class input gets one flag per top-level field, named after the field in kebab case,
|
|
85
|
-
with the field's description as its help, annotated before or after its checks. The field's
|
|
86
|
-
encoded JSON value decides the kind of flag:
|
|
102
|
+
A struct or class input gets one flag per top-level field, named after the field in kebab case, with the field's description as its help, annotated before or after its checks. The field's encoded JSON value decides the kind of flag:
|
|
87
103
|
|
|
88
|
-
| Encoded field
|
|
89
|
-
|
|
|
90
|
-
| string or template literal
|
|
91
|
-
| boolean
|
|
92
|
-
| union of string literals, or string enum | `--kind <choice>`, one of the values
|
|
93
|
-
| array of strings, numbers or choices
|
|
94
|
-
| anything else, numbers included
|
|
104
|
+
| Encoded field | Flag, for a field `tags`, `read`, … |
|
|
105
|
+
| --- | --- |
|
|
106
|
+
| string or template literal | `--name <string>` |
|
|
107
|
+
| boolean | `--read`, a switch: `--no-read` or `--read=false` sends `false` |
|
|
108
|
+
| union of string literals, or string enum | `--kind <choice>`, one of the values |
|
|
109
|
+
| array of strings, numbers or choices | `--tags <value>`, repeated: one element per occurrence |
|
|
110
|
+
| anything else, numbers included | `--owner <value>`: JSON the field accepts, or else the text |
|
|
95
111
|
|
|
96
112
|
- A switch left out is `false` for a required field, and leaves an optional one out.
|
|
97
|
-
- `--help` adds, after the field's description, `Repeat for each value.` on a repeated flag,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
-
|
|
102
|
-
`Schema.FiniteFromString`.
|
|
103
|
-
- A value flag reads its text as JSON when the field accepts that kind of JSON value
|
|
104
|
-
(`--count 2`, `--owner '{"id":"x"}'`, `--points '[{"x":1}]'`), and keeps it as text otherwise
|
|
105
|
-
(`--limit auto`): quote a string that would read as JSON, `--id '"123"'` for a
|
|
106
|
-
`String | Number` field.
|
|
107
|
-
- A repeated flag given no occurrence sends `[]` for a required `Schema.Array` field, and leaves
|
|
108
|
-
an optional field out, where `--tags '[]'` alone sends an empty list.
|
|
109
|
-
- An input that is not a struct of named fields (a union, a record, a scalar) gets one
|
|
110
|
-
`--input <value>` flag carrying the whole encoded input. Left off, the input is `{}`.
|
|
113
|
+
- `--help` adds, after the field's description, `Repeat for each value.` on a repeated flag, `--no-read sends false.` on an optional switch, `Left out, it sends false.` on a required one, and `A JSON array.` or `A JSON object.` on a value that is one, an array of objects with its fields: `A JSON array of {id, approved?}.`
|
|
114
|
+
- A flag takes the encoded value, which the action's schema then decodes: `"21"` for `Schema.FiniteFromString`.
|
|
115
|
+
- A value flag reads its text as JSON when the field accepts that kind of JSON value (`--count 2`, `--owner '{"id":"x"}'`, `--points '[{"x":1}]'`), and keeps it as text otherwise (`--limit auto`): quote a string that would read as JSON, `--id '"123"'` for a `String | Number` field.
|
|
116
|
+
- A repeated flag given no occurrence sends `[]` for a required `Schema.Array` field, and leaves an optional field out, where `--tags '[]'` alone sends an empty list.
|
|
117
|
+
- An input that is not a struct of named fields (a union, a record, a scalar) gets one `--input <value>` flag carrying the whole encoded input. Left off, the input is `{}`.
|
|
111
118
|
|
|
112
119
|
### Arguments, short flags and stdin
|
|
113
120
|
|
|
114
|
-
`description`, `render`, `positional`, `aliases` and `stdin` shape one command; in an aggregate
|
|
115
|
-
they go under `commands`, keyed by the action's name. Each one's rules are on its own
|
|
116
|
-
declaration:
|
|
121
|
+
`description`, `render`, `positional`, `aliases` and `stdin` shape one command; in an aggregate they go under `commands`, keyed by the action's name. Each one's rules are on its own declaration:
|
|
117
122
|
|
|
118
123
|
```ts example=assistant/src/commands.ts
|
|
119
124
|
import * as ActionCli from "@gjermundgaraba/effect-actions/ActionCli";
|
|
125
|
+
|
|
120
126
|
import { identity } from "./identity/handlers.js";
|
|
121
127
|
import { tasks } from "./tasks/handlers.js";
|
|
122
128
|
|
|
@@ -130,11 +136,15 @@ export const commands = ActionCli.make([tasks, identity], {
|
|
|
130
136
|
// per task for a person, and adds `--json` for the JSON a script reads. `description` is the
|
|
131
137
|
// command's help, saying what a line holds; the action's stays what a model reads.
|
|
132
138
|
listTasks: {
|
|
133
|
-
description:
|
|
139
|
+
description:
|
|
140
|
+
"Print one line per task: [x] when done, then its id, title and assignee.",
|
|
134
141
|
aliases: { assignee: "a" },
|
|
135
|
-
render: ({ tasks }) =>
|
|
136
|
-
|
|
137
|
-
.map(
|
|
142
|
+
render: ({ tasks: listed }) =>
|
|
143
|
+
listed
|
|
144
|
+
.map(
|
|
145
|
+
(task) =>
|
|
146
|
+
`${task.done ? "[x]" : "[ ]"} ${task.id} ${task.title} (${task.assignee})`
|
|
147
|
+
)
|
|
138
148
|
.join("\n"),
|
|
139
149
|
},
|
|
140
150
|
// `assistant complete-task 2`: an argument instead of `--id 2`.
|
|
@@ -147,25 +157,10 @@ export const commands = ActionCli.make([tasks, identity], {
|
|
|
147
157
|
|
|
148
158
|
### Add a command of your own
|
|
149
159
|
|
|
150
|
-
Work every surface should offer, a bulk import for one, is an action first: a command of your
|
|
151
|
-
own then only reads the file and calls it. For a syntax the options cannot express, such as a
|
|
152
|
-
flag named apart from its field, build the command with native `Command.make`, `Flag` and
|
|
153
|
-
`Argument`, and pass it in `subcommands`, whose rules are on `ActionCli.Options`; pipe its
|
|
154
|
-
Effect through `toUserError` to fail as a derived command does.
|
|
155
|
-
In [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts),
|
|
156
|
-
`notes import <file>` reads `Argument.FileText` and calls `importNotes`, and
|
|
157
|
-
`notes export <file>` takes `Argument.String("file")` and calls two actions.
|
|
160
|
+
Work every surface should offer, a bulk import for one, is an action first: a command of your own then only reads the file and calls it. For a syntax the options cannot express, such as a flag named apart from its field, build the command with native `Command.make`, `Flag` and `Argument`, and pass it in `subcommands`, whose rules are on `ActionCli.Options`; pipe its Effect through `toUserError` to fail as a derived command does. In [notes-service/src/commands.ts](../examples/notes-service/src/commands.ts), `notes import <file>` reads `Argument.FileText` and calls `importNotes`, and `notes export <file>` takes `Argument.String("file")` and calls two actions.
|
|
158
161
|
|
|
159
162
|
## Name the caller
|
|
160
163
|
|
|
161
|
-
`Command.provideSync(CurrentCaller, operator)` names the caller on the command in
|
|
162
|
-
|
|
163
|
-
`
|
|
164
|
-
([Action.md](Action.md#say-who-may-call-an-action)).
|
|
165
|
-
|
|
166
|
-
- **A trusted operator is a type, not a flag.** Give the identity a variant only a host builds,
|
|
167
|
-
kept out of every verifier's return type, and admit it in the shared `authorize`:
|
|
168
|
-
[assistant/src/identity/Identity.ts](../examples/assistant/src/identity/Identity.ts) with
|
|
169
|
-
[assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts), or
|
|
170
|
-
[kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) with
|
|
171
|
-
[kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts).
|
|
164
|
+
`Command.provideSync(CurrentCaller, operator)` names the caller on the command in [assistant/src/cli.ts](../examples/assistant/src/cli.ts), or `Command.provideEffect(CurrentCaller, load)` reads it when an action runs ([Action.md](Action.md#say-who-may-call-an-action)).
|
|
165
|
+
|
|
166
|
+
- **A trusted operator is a type, not a flag.** Give the identity a variant only a host builds, kept out of every verifier's return type, and admit it in the shared `authorize`: [assistant/src/identity/Identity.ts](../examples/assistant/src/identity/Identity.ts) with [assistant/src/identity/authorize.ts](../examples/assistant/src/identity/authorize.ts), or [kitchen-sink/src/admin/Admin.ts](../examples/kitchen-sink/src/admin/Admin.ts) with [kitchen-sink/src/ops.ts](../examples/kitchen-sink/src/ops.ts).
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# ActionHttp failure modes
|
|
2
2
|
|
|
3
|
-
What you see when something is wrong, and the fix. Search for a fragment of the message.
|
|
4
|
-
The tasks are in [ActionHttp.md](ActionHttp.md).
|
|
3
|
+
What you see when something is wrong, and the fix. Search for a fragment of the message. The tasks are in [ActionHttp.md](ActionHttp.md).
|
|
5
4
|
|
|
6
5
|
## Binding and serving
|
|
7
6
|
|
|
8
7
|
- A route returns 404, which a client method reports as an `HttpClientError` with `DecodeError` or `StatusCodeError`: the path lacks the prefix, no `ActionHttp.layer` call serves the action, or the binding does not hold that very action value, as when a test declares the contract again.
|
|
9
|
-
- `Method 'POST' already declared for route '<prefix>/<action>'` when the host builds: two `ActionHttp.layer` calls serve one action; give each layer its own `actions`.
|
|
8
|
+
- `Method 'POST' already declared for route '<prefix>/<action>'`, or `'<prefix>/events/<event>'`, when the host builds: two `ActionHttp.layer` calls serve one action or event type; give each layer its own `actions`.
|
|
10
9
|
- `Duplicate OpenAPI operationId: <name>` from `OpenApi.fromApi`: two bindings combined into one `HttpApi` share an action name; rename one, or document each binding alone.
|
|
11
10
|
- `Type 'Handlers<…>' is not assignable to type '`Endpoint not handled: ${string}`'` at `HttpApiBuilder.group(binding.api, …)`, or `Service not found: effect-actions/Authentication/Security/…` when the host builds: a binding is served with the native `HttpApiBuilder`; serve it with `ActionHttp.layer`.
|
|
11
|
+
- `event: effect/http-api/stream/failure` ends an SSE stream: Effect's failure event, an event's source refusing the subscriber after the stream started, or an action answering progress or a stream failing after its first event; its `data` is the failure as an encoded `Cause`, which `ActionHttp.client`'s `watch` or method fails with as declared.
|
|
12
12
|
- `Unsupported content-type: <type>` as a 415 body: Effect refuses a `Content-Type` other than `application/json`, after authentication and before the handler; send `application/json`.
|
|
13
13
|
|
|
14
14
|
## Types
|