@effect/platform-node-shared 4.0.0-beta.99 → 4.0.0-rc.109
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/AGENTS.md +395 -0
- package/CLAUDE.md +395 -0
- package/README.md +10 -3
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +39 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +5 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +165 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +110 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +160 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +24 -0
- package/dist/NodeChildProcessSpawner.d.ts.map +1 -1
- package/dist/NodeChildProcessSpawner.js +14 -10
- package/dist/NodeChildProcessSpawner.js.map +1 -1
- package/dist/NodeClusterSocket.js.map +1 -1
- package/dist/NodeCrypto.js.map +1 -1
- package/dist/NodeFileSystem.d.ts.map +1 -1
- package/dist/NodeFileSystem.js +7 -7
- package/dist/NodeFileSystem.js.map +1 -1
- package/dist/NodeHttpCompression.d.ts +36 -0
- package/dist/NodeHttpCompression.d.ts.map +1 -0
- package/dist/NodeHttpCompression.js +146 -0
- package/dist/NodeHttpCompression.js.map +1 -0
- package/dist/NodePath.d.ts.map +1 -1
- package/dist/NodePath.js +23 -20
- package/dist/NodePath.js.map +1 -1
- package/dist/NodeRuntime.js.map +1 -1
- package/dist/NodeSink.js.map +1 -1
- package/dist/NodeSocket.js.map +1 -1
- package/dist/NodeSocketServer.d.ts.map +1 -1
- package/dist/NodeSocketServer.js +29 -19
- package/dist/NodeSocketServer.js.map +1 -1
- package/dist/NodeStdio.d.ts.map +1 -1
- package/dist/NodeStdio.js +2 -0
- package/dist/NodeStdio.js.map +1 -1
- package/dist/NodeStream.d.ts.map +1 -1
- package/dist/NodeStream.js +10 -7
- package/dist/NodeStream.js.map +1 -1
- package/dist/NodeTerminal.d.ts.map +1 -1
- package/dist/NodeTerminal.js +63 -11
- package/dist/NodeTerminal.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/nodeChildProcessSpawner.d.ts +4 -0
- package/dist/internal/nodeChildProcessSpawner.d.ts.map +1 -0
- package/dist/internal/nodeChildProcessSpawner.js +10 -0
- package/dist/internal/nodeChildProcessSpawner.js.map +1 -0
- package/dist/internal/utils.js.map +1 -1
- package/package.json +11 -9
- package/src/NodeChildProcessSpawner.ts +19 -12
- package/src/NodeFileSystem.ts +16 -12
- package/src/NodeHttpCompression.ts +153 -0
- package/src/NodePath.ts +25 -27
- package/src/NodeSocketServer.ts +32 -19
- package/src/NodeStdio.ts +2 -0
- package/src/NodeStream.ts +10 -7
- package/src/NodeTerminal.ts +62 -13
- package/src/index.ts +5 -0
- package/src/internal/nodeChildProcessSpawner.ts +16 -0
- package/src/internal/utils.ts +1 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
import { Model } from "effect/unstable/schema"
|
|
3
|
+
|
|
4
|
+
export const UserId = Schema.String.pipe(Schema.brand("UserId"))
|
|
5
|
+
export type UserId = typeof UserId.Type
|
|
6
|
+
|
|
7
|
+
// `Model.Class` derives variants for the database (`User`, `User.insert`,
|
|
8
|
+
// `User.update`) and the JSON API (`User.json`, `User.jsonCreate`,
|
|
9
|
+
// `User.jsonUpdate`) from a single field declaration.
|
|
10
|
+
export class User extends Model.Class<User>("User")({
|
|
11
|
+
// A UUID v4 primary key generated by the application on insert. It is
|
|
12
|
+
// excluded from the `jsonCreate` / `jsonUpdate` variants, so API clients can
|
|
13
|
+
// never set it.
|
|
14
|
+
id: Model.UuidV4Insert(UserId),
|
|
15
|
+
name: Schema.String,
|
|
16
|
+
email: Schema.String,
|
|
17
|
+
// Timestamps are managed by the model: set on insert, refreshed on update,
|
|
18
|
+
// and also excluded from the JSON create / update variants.
|
|
19
|
+
createdAt: Model.DateTimeInsert,
|
|
20
|
+
updatedAt: Model.DateTimeUpdate
|
|
21
|
+
}) {}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
export class UserNotFound extends Schema.TaggedError<UserNotFound>()(
|
|
4
|
+
"UserNotFound",
|
|
5
|
+
{},
|
|
6
|
+
// You can specify the status code for this error inline
|
|
7
|
+
{ httpApiStatus: 404 }
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
export class SearchQueryTooShort
|
|
11
|
+
extends Schema.TaggedError<SearchQueryTooShort>()("SearchQueryTooShort", {}, { httpApiStatus: 422 })
|
|
12
|
+
{
|
|
13
|
+
static readonly minimumLength = 2
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Create a wrapper error class for all errors in the Users API.
|
|
17
|
+
//
|
|
18
|
+
// This prevents adding too many error types to services / endpoint definitions.
|
|
19
|
+
//
|
|
20
|
+
export class UsersError extends Schema.TaggedError<UsersError>()("UsersError", {
|
|
21
|
+
reason: Schema.Union([UserNotFound, SearchQueryTooShort])
|
|
22
|
+
}) {}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DateTime, Effect, Layer, Redacted } from "effect"
|
|
2
|
+
import { Authorization, CurrentUser, Unauthorized } from "../api/Authorization.ts"
|
|
3
|
+
import { User, UserId } from "../domain/User.ts"
|
|
4
|
+
|
|
5
|
+
const fixedTimestamp = DateTime.makeUnsafe("2026-01-01T00:00:00Z")
|
|
6
|
+
const devUser = new User({
|
|
7
|
+
id: UserId.make("bf3dbe33-0ad2-4c9c-9c9e-733e57bdcbee"),
|
|
8
|
+
name: "Dev User",
|
|
9
|
+
email: "dev@acme.com",
|
|
10
|
+
createdAt: fixedTimestamp,
|
|
11
|
+
updatedAt: fixedTimestamp
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
// The implementation of the Authorization middleware. It is seperate from the
|
|
15
|
+
// service definition to avoid leaking it into a client.
|
|
16
|
+
export const AuthorizationLayer = Layer.effect(
|
|
17
|
+
Authorization,
|
|
18
|
+
Effect.gen(function*() {
|
|
19
|
+
// Here you could access services required by the middleware, like a
|
|
20
|
+
// database or an external auth provider.
|
|
21
|
+
yield* Effect.logInfo("Starting Authorization middleware")
|
|
22
|
+
|
|
23
|
+
return Authorization.of({
|
|
24
|
+
bearer: Effect.fn(function*(httpEffect, { credential }) {
|
|
25
|
+
// Validate the token and return an Unauthorized error if it's invalid.
|
|
26
|
+
const token = Redacted.value(credential)
|
|
27
|
+
if (token !== "dev-token") {
|
|
28
|
+
return yield* new Unauthorized({ message: "Missing or invalid bearer token" })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Provide the current user to the rest of the stack. This will be
|
|
32
|
+
// available in any endpoint or middleware that runs after this one.
|
|
33
|
+
return yield* Effect.provideService(httpEffect, CurrentUser, devUser)
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Effect, Layer } from "effect"
|
|
2
|
+
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
|
|
3
|
+
import { Api } from "../../api/Api.ts"
|
|
4
|
+
import { CurrentUser } from "../../api/Authorization.ts"
|
|
5
|
+
import { AuthorizationLayer } from "../Authorization.ts"
|
|
6
|
+
import { Users } from "../Users.ts"
|
|
7
|
+
|
|
8
|
+
// The handlers without their dependencies provided, so tests can supply an
|
|
9
|
+
// alternative `Users` implementation.
|
|
10
|
+
export const UsersApiHandlersNoDeps = HttpApiBuilder.group(
|
|
11
|
+
Api,
|
|
12
|
+
"users",
|
|
13
|
+
Effect.fn(function*(handlers) {
|
|
14
|
+
const users = yield* Users
|
|
15
|
+
|
|
16
|
+
return handlers.handleAll({
|
|
17
|
+
list: ({ query }) =>
|
|
18
|
+
users.list(query.search).pipe(
|
|
19
|
+
// The list endpoint expects no errors, so we convert any potential
|
|
20
|
+
// errors into a 500 Internal Server Error.
|
|
21
|
+
Effect.orDie
|
|
22
|
+
),
|
|
23
|
+
search: Effect.fn(function*({ payload }) {
|
|
24
|
+
if (payload.search === "bad-request") {
|
|
25
|
+
// You can use the built in error types like any other
|
|
26
|
+
// Schema.TaggedError
|
|
27
|
+
return yield* new HttpApiError.RequestTimeout()
|
|
28
|
+
}
|
|
29
|
+
return yield* users.list(payload.search).pipe(
|
|
30
|
+
Effect.catchReason(
|
|
31
|
+
"UsersError",
|
|
32
|
+
"SearchQueryTooShort",
|
|
33
|
+
// Re-fail the "SearchQueryTooShort" reason
|
|
34
|
+
Effect.fail,
|
|
35
|
+
// All other reasons are unexpected, so we convert them into a 500
|
|
36
|
+
// Internal Server Error.
|
|
37
|
+
Effect.die
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
}),
|
|
41
|
+
getById: ({ params }) =>
|
|
42
|
+
users.getById(params.id).pipe(
|
|
43
|
+
// You can also use Effect.catchReasons to handle multiple error
|
|
44
|
+
// reasons at once
|
|
45
|
+
Effect.catchReasons("UsersError", {
|
|
46
|
+
UserNotFound: (e) => Effect.fail(e)
|
|
47
|
+
}, Effect.die)
|
|
48
|
+
),
|
|
49
|
+
create: ({ payload }) =>
|
|
50
|
+
users.create(payload).pipe(
|
|
51
|
+
Effect.orDie
|
|
52
|
+
// You could alse use Effect.unwrapReason to moves rror reasons up to
|
|
53
|
+
// the top level, so you can handle them with Effect.catch or
|
|
54
|
+
// Effect.catchTag etc.
|
|
55
|
+
//
|
|
56
|
+
// Effect.unwrapReason("UsersError"),
|
|
57
|
+
// Effect.catchTags({
|
|
58
|
+
// UserNotFound: Effect.die,
|
|
59
|
+
// SearchQueryTooShort: Effect.die
|
|
60
|
+
// })
|
|
61
|
+
),
|
|
62
|
+
update: ({ params, payload }) =>
|
|
63
|
+
users.update(params.id, payload).pipe(
|
|
64
|
+
Effect.catchReasons("UsersError", {
|
|
65
|
+
UserNotFound: (e) => Effect.fail(e)
|
|
66
|
+
}, Effect.die)
|
|
67
|
+
),
|
|
68
|
+
me: () =>
|
|
69
|
+
// The Authorization middleware provides the CurrentUser service, so we
|
|
70
|
+
// can access it here.
|
|
71
|
+
CurrentUser
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// The handlers with all dependencies provided, ready to serve. The SQL-backed
|
|
77
|
+
// `Users.layer` keeps the database wiring out of the server entrypoint.
|
|
78
|
+
export const UsersApiHandlers = UsersApiHandlersNoDeps.pipe(
|
|
79
|
+
Layer.provide([Users.layer, AuthorizationLayer])
|
|
80
|
+
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node"
|
|
2
|
+
import { Context, Effect, Layer, Schema } from "effect"
|
|
3
|
+
import { SqlClient, SqlModel, SqlSchema } from "effect/unstable/sql"
|
|
4
|
+
import { User } from "../domain/User.ts"
|
|
5
|
+
import type { UserId } from "../domain/User.ts"
|
|
6
|
+
import { SearchQueryTooShort, UserNotFound, UsersError } from "../domain/UserErrors.ts"
|
|
7
|
+
|
|
8
|
+
// The SqlClient layer determines which database the SQL implementation talks
|
|
9
|
+
// to. Swap it for another driver package to target a different database.
|
|
10
|
+
const SqlLayer = SqliteClient.layer({ filename: ":memory:" })
|
|
11
|
+
|
|
12
|
+
// Migrations are effects keyed by `<id>_<name>` that run once, in id order. A
|
|
13
|
+
// real application would keep each migration in its own file and load them
|
|
14
|
+
// with `SqliteMigrator.fromFileSystem` instead of an inline record.
|
|
15
|
+
const MigratorLayer = SqliteMigrator.layer({
|
|
16
|
+
loader: SqliteMigrator.fromRecord({
|
|
17
|
+
"0001_create_users": Effect.gen(function*() {
|
|
18
|
+
const sql = yield* SqlClient.SqlClient
|
|
19
|
+
yield* sql`
|
|
20
|
+
CREATE TABLE users (
|
|
21
|
+
id TEXT PRIMARY KEY,
|
|
22
|
+
name TEXT NOT NULL,
|
|
23
|
+
email TEXT NOT NULL,
|
|
24
|
+
createdAt TEXT NOT NULL,
|
|
25
|
+
updatedAt TEXT NOT NULL
|
|
26
|
+
)
|
|
27
|
+
`
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
export class Users extends Context.Service<Users, {
|
|
33
|
+
list(search: string | undefined): Effect.Effect<Array<User>, UsersError>
|
|
34
|
+
getById(id: UserId): Effect.Effect<User, UsersError>
|
|
35
|
+
create(input: typeof User.jsonCreate.Type): Effect.Effect<User, UsersError>
|
|
36
|
+
update(id: UserId, input: typeof User.jsonUpdate.Type): Effect.Effect<User, UsersError>
|
|
37
|
+
}>()("acme/Users") {
|
|
38
|
+
// The SQL implementation only requires a `SqlClient`, so entrypoints and
|
|
39
|
+
// tests decide how the database is provided.
|
|
40
|
+
static readonly layerNoDeps = Layer.effect(
|
|
41
|
+
Users,
|
|
42
|
+
Effect.gen(function*() {
|
|
43
|
+
const sql = yield* SqlClient.SqlClient
|
|
44
|
+
|
|
45
|
+
// CRUD goes through a repository derived from the `User` model. Each
|
|
46
|
+
// operation uses the matching model variant to encode its input and
|
|
47
|
+
// decodes rows with the full model schema.
|
|
48
|
+
const repo = yield* SqlModel.makeRepository(User, {
|
|
49
|
+
tableName: "users",
|
|
50
|
+
spanPrefix: "Users",
|
|
51
|
+
idColumn: "id"
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// Queries the repository does not cover are written with the `sql` tag
|
|
55
|
+
// and decoded with the model schema.
|
|
56
|
+
const listAll = SqlSchema.findAll({
|
|
57
|
+
Request: Schema.Void,
|
|
58
|
+
Result: User,
|
|
59
|
+
execute: () => sql`SELECT * FROM users ORDER BY createdAt`
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const searchUsers = SqlSchema.findAll({
|
|
63
|
+
Request: Schema.String,
|
|
64
|
+
Result: User,
|
|
65
|
+
execute: (search) => {
|
|
66
|
+
const pattern = `%${search}%`
|
|
67
|
+
return sql`SELECT * FROM users WHERE name LIKE ${pattern} OR email LIKE ${pattern}`
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
const list = Effect.fn("Users.list")(function*(search: string | undefined) {
|
|
72
|
+
if (search === undefined || search.length === 0) {
|
|
73
|
+
return yield* Effect.orDie(listAll())
|
|
74
|
+
} else if (search.length < SearchQueryTooShort.minimumLength) {
|
|
75
|
+
return yield* new UsersError({
|
|
76
|
+
reason: new SearchQueryTooShort()
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
yield* Effect.annotateCurrentSpan({ search })
|
|
80
|
+
return yield* Effect.orDie(searchUsers(search))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const getById = Effect.fn("Users.getById")((id: UserId) =>
|
|
84
|
+
repo.findById(id).pipe(
|
|
85
|
+
Effect.catchTags({
|
|
86
|
+
NoSuchElementError: () => new UsersError({ reason: new UserNotFound() }),
|
|
87
|
+
// Database and encoding failures are unexpected, so treat them as
|
|
88
|
+
// defects to keep the service interface focused on domain errors.
|
|
89
|
+
SchemaError: Effect.die,
|
|
90
|
+
SqlError: Effect.die
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
const create = Effect.fn("Users.create")((input: typeof User.jsonCreate.Type) =>
|
|
96
|
+
// `User.insert.makeEffect` fills in the generated id and timestamps
|
|
97
|
+
// using the Effect clock, so tests can control them with `TestClock`.
|
|
98
|
+
User.insert.makeEffect(input).pipe(
|
|
99
|
+
Effect.flatMap(repo.insert),
|
|
100
|
+
Effect.orDie
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
|
|
105
|
+
// Ensure the user exists first, so a missing id fails with the domain
|
|
106
|
+
// error instead of a defect.
|
|
107
|
+
yield* getById(id)
|
|
108
|
+
const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
|
|
109
|
+
return yield* repo.update(update).pipe(Effect.orDie)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return Users.of({ list, getById, create, update })
|
|
113
|
+
})
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// The fully provided SQL implementation: the database client and migrations
|
|
117
|
+
// are implementation details, so this layer requires nothing.
|
|
118
|
+
static readonly layer: Layer.Layer<Users> = this.layerNoDeps.pipe(
|
|
119
|
+
Layer.provide(MigratorLayer.pipe(Layer.provideMerge(SqlLayer))),
|
|
120
|
+
Layer.orDie
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
// An in-memory implementation for tests, so the HTTP stack can be exercised
|
|
124
|
+
// without a database.
|
|
125
|
+
static readonly layerMemory = Layer.effect(
|
|
126
|
+
Users,
|
|
127
|
+
Effect.gen(function*() {
|
|
128
|
+
const users = new Map<UserId, User>()
|
|
129
|
+
|
|
130
|
+
const makeUser = (input: typeof User.jsonCreate.Type) =>
|
|
131
|
+
User.insert.makeEffect(input).pipe(
|
|
132
|
+
Effect.map((user) => new User(user)),
|
|
133
|
+
Effect.orDie
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
const admin = yield* makeUser({ name: "Admin", email: "admin@acme.dev" })
|
|
137
|
+
users.set(admin.id, admin)
|
|
138
|
+
|
|
139
|
+
const list = Effect.fn("Users.list")(function*(search: string | undefined) {
|
|
140
|
+
const allUsers = Array.from(users.values())
|
|
141
|
+
if (search === undefined || search.length === 0) {
|
|
142
|
+
return allUsers
|
|
143
|
+
} else if (search.length < SearchQueryTooShort.minimumLength) {
|
|
144
|
+
return yield* new UsersError({
|
|
145
|
+
reason: new SearchQueryTooShort()
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
yield* Effect.annotateCurrentSpan({ search })
|
|
149
|
+
const normalized = search.toLowerCase()
|
|
150
|
+
return allUsers.filter((user) =>
|
|
151
|
+
user.name.toLowerCase().includes(normalized) || user.email.toLowerCase().includes(normalized)
|
|
152
|
+
)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const getById = Effect.fn("Users.getById")(function*(id: UserId) {
|
|
156
|
+
yield* Effect.annotateCurrentSpan({ id })
|
|
157
|
+
const user = users.get(id)
|
|
158
|
+
if (user === undefined) {
|
|
159
|
+
return yield* new UsersError({
|
|
160
|
+
reason: new UserNotFound()
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
return user
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const create = Effect.fn("Users.create")(function*(input: typeof User.jsonCreate.Type) {
|
|
167
|
+
const user = yield* makeUser(input)
|
|
168
|
+
users.set(user.id, user)
|
|
169
|
+
return user
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
|
|
173
|
+
const existing = yield* getById(id)
|
|
174
|
+
const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
|
|
175
|
+
const updated = new User({ ...existing, ...update })
|
|
176
|
+
users.set(id, updated)
|
|
177
|
+
return updated
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
return Users.of({ list, getById, create, update })
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Working with child processes
|
|
3
|
+
*
|
|
4
|
+
* This example shows how to collect process output, compose pipelines, and stream long-running command output.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeServices } from "@effect/platform-node"
|
|
7
|
+
import { Console, Context, Effect, Layer, Schema, Stream, String } from "effect"
|
|
8
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|
9
|
+
|
|
10
|
+
export class DevToolsError extends Schema.TaggedError<DevToolsError>()("DevToolsError", {
|
|
11
|
+
cause: Schema.Defect()
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
export class DevTools extends Context.Service<DevTools, {
|
|
15
|
+
readonly nodeVersion: Effect.Effect<string, DevToolsError>
|
|
16
|
+
readonly recentCommitSubjects: Effect.Effect<ReadonlyArray<string>, DevToolsError>
|
|
17
|
+
readonly runLintFix: Effect.Effect<void, DevToolsError>
|
|
18
|
+
changedTypeScriptFiles(baseRef: string): Effect.Effect<ReadonlyArray<string>, DevToolsError>
|
|
19
|
+
}>()("docs/DevTools") {
|
|
20
|
+
static readonly layer = Layer.effect(
|
|
21
|
+
DevTools,
|
|
22
|
+
Effect.gen(function*() {
|
|
23
|
+
// To run child processes, we need access to a `ChildProcessSpawner`.
|
|
24
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
25
|
+
|
|
26
|
+
// Use `spawner.string` when you want to collect the entire output of a
|
|
27
|
+
// command as a string. This runs `node --version` and collects the
|
|
28
|
+
// output.
|
|
29
|
+
const nodeVersion = spawner.string(
|
|
30
|
+
ChildProcess.make("node", ["--version"])
|
|
31
|
+
).pipe(
|
|
32
|
+
Effect.map(String.trim),
|
|
33
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const changedTypeScriptFiles = Effect.fn("DevTools.changedTypeScriptFiles")(function*(baseRef: string) {
|
|
37
|
+
yield* Effect.annotateCurrentSpan({ baseRef })
|
|
38
|
+
|
|
39
|
+
// `spawner.lines` is a convenience helper for line-oriented command
|
|
40
|
+
// output.
|
|
41
|
+
const files = yield* spawner.lines(
|
|
42
|
+
ChildProcess.make("git", ["diff", "--name-only", `${baseRef}...HEAD`])
|
|
43
|
+
).pipe(
|
|
44
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return files.filter((file) => file.endsWith(".ts"))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Build a pipeline from two command values. This runs:
|
|
51
|
+
// `git log --pretty=format:%s -n 20 | head -n 5`
|
|
52
|
+
const recentCommitSubjects = spawner.lines(
|
|
53
|
+
ChildProcess.make("git", ["log", "--pretty=format:%s", "-n", "20"]).pipe(
|
|
54
|
+
ChildProcess.pipeTo(ChildProcess.make("head", ["-n", "5"]))
|
|
55
|
+
)
|
|
56
|
+
).pipe(
|
|
57
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
const runLintFix = Effect.gen(function*() {
|
|
61
|
+
// Use `spawn` when you want the process handle and stream output while
|
|
62
|
+
// the process is still running.
|
|
63
|
+
const handle = yield* spawner.spawn(
|
|
64
|
+
ChildProcess.make("pnpm", ["lint-fix"], {
|
|
65
|
+
env: { FORCE_COLOR: "1" },
|
|
66
|
+
extendEnv: true
|
|
67
|
+
})
|
|
68
|
+
).pipe(
|
|
69
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
yield* handle.all.pipe(
|
|
73
|
+
Stream.decodeText(),
|
|
74
|
+
Stream.splitLines,
|
|
75
|
+
Stream.runForEach((line) => Console.log(`[lint-fix] ${line}`)),
|
|
76
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const exitCode = yield* handle.exitCode.pipe(
|
|
80
|
+
Effect.mapError((cause) => new DevToolsError({ cause }))
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
|
|
84
|
+
return yield* new DevToolsError({
|
|
85
|
+
cause: new Error(`pnpm lint-fix failed with exit code ${exitCode}`)
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}).pipe(
|
|
89
|
+
// `spawner.spawn` adds a `Scope` requirement to manage the lifecycle of
|
|
90
|
+
// the child process. We can use `Effect.scoped` to provide a `Scope`
|
|
91
|
+
// and close it when the effect completes.
|
|
92
|
+
Effect.scoped
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return DevTools.of({
|
|
96
|
+
nodeVersion,
|
|
97
|
+
changedTypeScriptFiles,
|
|
98
|
+
recentCommitSubjects,
|
|
99
|
+
runLintFix
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
).pipe(
|
|
103
|
+
// Provide the `ChildProcessSpawner` dependency from `NodeServices.layer`.
|
|
104
|
+
Layer.provide(NodeServices.layer)
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const program = Effect.gen(function*() {
|
|
109
|
+
const tools = yield* DevTools
|
|
110
|
+
|
|
111
|
+
const version = yield* tools.nodeVersion
|
|
112
|
+
yield* Effect.log(`node=${version}`)
|
|
113
|
+
}).pipe(
|
|
114
|
+
// `ChildProcess` requires a platform implementation of
|
|
115
|
+
// `ChildProcessSpawner`. In Node.js, `NodeServices.layer` provides it.
|
|
116
|
+
Effect.provide(DevTools.layer)
|
|
117
|
+
)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Getting started with Effect CLI modules
|
|
3
|
+
*
|
|
4
|
+
* Build a command-line app with typed arguments and flags, then wire subcommand
|
|
5
|
+
* handlers into a single executable command.
|
|
6
|
+
*/
|
|
7
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node"
|
|
8
|
+
import { Console, Effect, Option, Schema } from "effect"
|
|
9
|
+
import { Argument, Command, Flag } from "effect/unstable/cli"
|
|
10
|
+
|
|
11
|
+
// You can define flags outside of commands and reuse them across multiple
|
|
12
|
+
// commands.
|
|
13
|
+
const workspace = Flag.string("workspace").pipe(
|
|
14
|
+
Flag.withAlias("w"),
|
|
15
|
+
Flag.withDescription("Workspace to operate on"),
|
|
16
|
+
Flag.withDefault("personal")
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
// Start with a root command and explicitly share the parent flags that should
|
|
20
|
+
// be available to all subcommands.
|
|
21
|
+
const tasks = Command.make("tasks").pipe(
|
|
22
|
+
Command.withSharedFlags({
|
|
23
|
+
workspace,
|
|
24
|
+
verbose: Flag.boolean("verbose").pipe(
|
|
25
|
+
Flag.withAlias("v"),
|
|
26
|
+
Flag.withDescription("Print diagnostic output")
|
|
27
|
+
)
|
|
28
|
+
}),
|
|
29
|
+
Command.withDescription("Track and manage tasks")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Arguments and flags parse plain strings; use `withSchema` to validate or
|
|
33
|
+
// transform the parsed value with any schema.
|
|
34
|
+
const Email = Schema.String.pipe(
|
|
35
|
+
Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
|
|
36
|
+
message: "Expected a valid email address"
|
|
37
|
+
}))
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const create = Command.make(
|
|
41
|
+
"create",
|
|
42
|
+
{
|
|
43
|
+
title: Argument.string("title").pipe(
|
|
44
|
+
Argument.withDescription("Task title"),
|
|
45
|
+
// Reject empty titles at parse time, so the handler only ever sees
|
|
46
|
+
// valid input
|
|
47
|
+
Argument.withSchema(Schema.NonEmptyString)
|
|
48
|
+
),
|
|
49
|
+
priority: Flag.choice("priority", ["low", "normal", "high"]).pipe(
|
|
50
|
+
Flag.withDescription("Priority for the new task"),
|
|
51
|
+
Flag.withDefault("normal")
|
|
52
|
+
),
|
|
53
|
+
assignee: Flag.string("assignee").pipe(
|
|
54
|
+
Flag.withDescription("Email address of the person to assign"),
|
|
55
|
+
Flag.withSchema(Email),
|
|
56
|
+
Flag.optional
|
|
57
|
+
)
|
|
58
|
+
},
|
|
59
|
+
Effect.fn(function*({ assignee, priority, title }) {
|
|
60
|
+
// Subcommands can read parent command input by yielding the parent command.
|
|
61
|
+
const root = yield* tasks
|
|
62
|
+
|
|
63
|
+
if (root.verbose) {
|
|
64
|
+
yield* Console.log(`workspace=${root.workspace} action=create`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
yield* Console.log(`Created "${title}" in ${root.workspace} with ${priority} priority`)
|
|
68
|
+
|
|
69
|
+
if (Option.isSome(assignee)) {
|
|
70
|
+
yield* Console.log(`Assigned to ${assignee.value}`)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
).pipe(
|
|
74
|
+
Command.withDescription("Create a task"),
|
|
75
|
+
Command.withExamples([
|
|
76
|
+
{
|
|
77
|
+
command: "tasks create \"Ship 4.0\" --priority high",
|
|
78
|
+
description: "Create a high-priority task"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
command: "tasks create \"Ship 4.0\" --assignee dev@acme.com",
|
|
82
|
+
description: "Create a task assigned to a team member"
|
|
83
|
+
}
|
|
84
|
+
])
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const list = Command.make(
|
|
88
|
+
"list",
|
|
89
|
+
{
|
|
90
|
+
status: Flag.choice("status", ["open", "done", "all"]).pipe(
|
|
91
|
+
Flag.withDescription("Filter tasks by status"),
|
|
92
|
+
Flag.withDefault("open")
|
|
93
|
+
),
|
|
94
|
+
json: Flag.boolean("json").pipe(
|
|
95
|
+
Flag.withDescription("Print machine-readable output")
|
|
96
|
+
)
|
|
97
|
+
},
|
|
98
|
+
Effect.fn(function*({ status, json }) {
|
|
99
|
+
const root = yield* tasks
|
|
100
|
+
const items = [
|
|
101
|
+
{ title: "Ship 4.0", status: "open" },
|
|
102
|
+
{ title: "Update onboarding guide", status: "done" }
|
|
103
|
+
] as const
|
|
104
|
+
const filtered = status === "all"
|
|
105
|
+
? items
|
|
106
|
+
: items.filter((item) => item.status === status)
|
|
107
|
+
|
|
108
|
+
if (root.verbose) {
|
|
109
|
+
yield* Console.log(`workspace=${root.workspace} action=list`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (json) {
|
|
113
|
+
yield* Console.log(JSON.stringify(
|
|
114
|
+
{
|
|
115
|
+
workspace: root.workspace,
|
|
116
|
+
status,
|
|
117
|
+
items: filtered
|
|
118
|
+
},
|
|
119
|
+
null,
|
|
120
|
+
2
|
|
121
|
+
))
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
yield* Console.log(`Listing ${status} tasks in ${root.workspace}`)
|
|
126
|
+
if (filtered.length === 0) {
|
|
127
|
+
yield* Console.log("- No tasks found")
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const item of filtered) {
|
|
132
|
+
yield* Console.log(`- ${item.title}`)
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
).pipe(
|
|
136
|
+
Command.withDescription("List tasks"),
|
|
137
|
+
Command.withAlias("ls"),
|
|
138
|
+
Command.withExamples([
|
|
139
|
+
{
|
|
140
|
+
command: "tasks --workspace team-a list --status open",
|
|
141
|
+
description: "List open tasks in a specific workspace"
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
command: "tasks --workspace team-b ls --status open",
|
|
145
|
+
description: "List open tasks in another workspace"
|
|
146
|
+
}
|
|
147
|
+
])
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
// Finally, compose the subcommands into a single command and then run it.
|
|
151
|
+
tasks.pipe(
|
|
152
|
+
Command.withSubcommands([create, list]),
|
|
153
|
+
Command.run({
|
|
154
|
+
version: "1.0.0"
|
|
155
|
+
}),
|
|
156
|
+
// Provide the services for the platform you are targeting. In this case,
|
|
157
|
+
// Node.js
|
|
158
|
+
Effect.provide(NodeServices.layer),
|
|
159
|
+
NodeRuntime.runMain
|
|
160
|
+
)
|