Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

more like effect-http-play #11

Draft
wants to merge 11 commits into
base: r/vertical-slice
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"workbench.editor.labelFormat": "short",
"explorer.sortOrderLexicographicOptions": "upper",
"typescript.preferences.includePackageJsonAutoImports": "on",
"typescript.preferences.autoImportFileExcludePatterns": [
Expand Down
17 changes: 9 additions & 8 deletions api/src/Users.controllers.ts → api/src/Accounts.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { matchFor } from "api/lib/routing.js"
import { Q, UserRepo } from "api/services.js"
import { Array } from "effect"
import { Effect, Order } from "effect-app"
import { UsersRsc } from "resources.js"
import type { UserView } from "resources/views.js"
import { Array, Effect, Order } from "effect-app"
import { AccountsApi } from "resources.js"
import { UserRepo } from "./Accounts/UserRepo.js"
import type { UserView } from "./Accounts/UserView.js"
import { Q } from "./services.js"

export default matchFor(UsersRsc)([
export default matchFor(AccountsApi)([
UserRepo.Default
], ({ IndexUsers }) =>
], ({ GetMe, Index }) =>
Effect.gen(function*() {
const userRepo = yield* UserRepo
return {
IndexUsers: IndexUsers((req) =>
GetMe: GetMe(userRepo.getCurrentUser),
Index: Index((req) =>
userRepo
.query(Q.where("id", "in", req.filterByIds))
.pipe(Effect.andThen((users) => ({
Expand Down
20 changes: 20 additions & 0 deletions api/src/Accounts/Api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { User, UserId } from "Domain/User.js"
import { NotFoundError } from "effect-app/client"
import { S } from "lib/resources.js"
import { UserView } from "./UserView.js"

export class GetMe extends S.Req<GetMe>()("GetMe", {}, { success: User, failure: NotFoundError }) {}

export class Index extends S.Req<Index>()("Index", {
filterByIds: S.NonEmptyArray(UserId)
}, {
allowAnonymous: true,
allowRoles: ["user"],
success: S.Struct({
users: S.Array(UserView)
})
}) {}

//// codegen:start {preset: meta, sourcePrefix: src/User/}
export const meta = { moduleName: "Me" } as const
// codegen:end
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { Model } from "@effect-app/infra"
import { NotFoundError, NotLoggedInError } from "@effect-app/infra/errors"
import { generate } from "@effect-app/infra/test"
import { RepoConfig } from "api/config.js"
import type { UserId } from "Domain/User.js"
import { User } from "Domain/User.js"
import { RepoDefault } from "api/lib/layers.js"
import { Q, UserProfile } from "api/services.js"
import { Array, Effect, Exit, Layer, Option, pipe, Request, RequestResolver, S } from "effect-app"
import { fakerArb } from "effect-app/faker"
import { Email } from "effect-app/Schema"
import fc from "fast-check"
import type { UserId } from "models/User.js"
import { User } from "models/User.js"
import { Q } from "../lib.js"
import { UserProfile } from "../UserProfile.js"

export type UserSeed = "sample" | ""

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { UserId } from "Domain/User.js"
import { Effect, Exit, Request, RequestResolver } from "effect"
import { Array, Option, pipe, S } from "effect-app"
import { ApiConfig, NotFoundError } from "effect-app/client"
import { HttpClient } from "effect-app/http"
import { type Schema } from "effect-app/Schema"
import { UserId } from "models/User.js"
import { clientFor } from "resources/lib.js"
import * as UsersRsc from "../Users.js"
import { UserView } from "../views/UserView.js"
import { clientFor } from "lib/resources.js"
import { Index } from "./Api.js"
import { UserView } from "./UserView.js"

interface GetUserViewById extends Request.Request<UserView, NotFoundError<"User">> {
readonly _tag: "GetUserViewById"
readonly id: UserId
}
const GetUserViewById = Request.tagged<GetUserViewById>("GetUserViewById")

const userClient = clientFor(UsersRsc)
const usersApi = clientFor({ Index })

const getUserViewByIdResolver = RequestResolver
.makeBatched((requests: GetUserViewById[]) =>
userClient
.IndexUsers
usersApi
.Index
.handler({ filterByIds: pipe(requests.map((_) => _.id), Array.toNonEmptyArray, Option.getOrUndefined)! })
.pipe(
Effect.andThen(({ users }) =>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { User } from "models/User.js"
import { S } from "resources/lib.js"
import { User } from "Domain/User.js"
import { S } from "lib/resources.js"

export class UserView extends S.ExtendedClass<UserView, UserView.Encoded>()({
...User.pick("id", "role"),
Expand Down
18 changes: 10 additions & 8 deletions api/src/Blog.controllers.ts → api/src/Blog.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { OperationsDefault } from "api/lib/layers.js"
import { matchFor } from "api/lib/routing.js"
import { BlogPostRepo, Events, Operations, UserRepo } from "api/services.js"
import { Events, Operations } from "api/services.js"
import { Duration, Effect, Schedule } from "effect"
import { Option } from "effect-app"
import { NonEmptyString2k, NonNegativeInt } from "effect-app/Schema"
import { BlogPost } from "models/Blog.js"
import { BlogRsc } from "resources.js"
import { BogusEvent } from "resources/Events.js"
import { OperationsDefault } from "./lib/layers.js"
import { BlogApi } from "resources.js"
import { UserRepo } from "./Accounts/UserRepo.js"
import { BlogPostRepo } from "./Blog/BlogPostRepo.js"
import { BlogPost } from "./Domain/Blog.js"
import { BogusEvent } from "./Domain/Events.js"

export default matchFor(BlogRsc)([
export default matchFor(BlogApi)([
BlogPostRepo.Default,
UserRepo.Default,
OperationsDefault,
Events.Default
], ({ CreatePost, FindPost, GetPosts, PublishPost }) =>
], ({ CreatePost, FindPost, Index, PublishPost }) =>
Effect.gen(function*() {
const blogPostRepo = yield* BlogPostRepo
const userRepo = yield* UserRepo
Expand All @@ -27,7 +29,7 @@ export default matchFor(BlogRsc)([
.pipe(Effect.andThen(Option.getOrNull))
),

GetPosts: GetPosts(
Index: Index(
blogPostRepo
.all
.pipe(Effect.andThen((items) => ({ items })))
Expand Down
10 changes: 5 additions & 5 deletions api/src/resources/Blog.ts → api/src/Blog/Api.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { BlogPost, BlogPostId } from "Domain/Blog.js"
import { S } from "lib/resources.js"
import { InvalidStateError, NotFoundError, OptimisticConcurrencyException } from "effect-app/client"
import { OperationId } from "effect-app/Operations"
import { BlogPost, BlogPostId } from "models/Blog.js"
import { S } from "./lib.js"
import { BlogPostView } from "./views.js"
import { BlogPostView } from "./PostView.js"

export class CreatePost extends S.Req<CreatePost>()("CreatePost", BlogPost.pick("title", "body"), {
allowRoles: ["user"],
Expand All @@ -14,7 +14,7 @@ export class FindPost extends S.Req<FindPost>()("FindPost", {
id: BlogPostId
}, { allowAnonymous: true, allowRoles: ["user"], success: S.NullOr(BlogPostView) }) {}

export class GetPosts extends S.Req<GetPosts>()("GetPosts", {}, {
export class Index extends S.Req<Index>()("Index", {}, {
allowAnonymous: true,
allowRoles: ["user"],
success: S.Struct({
Expand All @@ -26,6 +26,6 @@ export class PublishPost extends S.Req<PublishPost>()("PublishPost", {
id: BlogPostId
}, { allowRoles: ["user"], success: OperationId, failure: S.Union(NotFoundError) }) {}

// codegen:start {preset: meta, sourcePrefix: src/resources/}
//// codegen:start {preset: meta, sourcePrefix: src/Blog/}
export const meta = { moduleName: "Blog" } as const
// codegen:end
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Model } from "@effect-app/infra"
import { UserRepo } from "api/Accounts/UserRepo.js"
import { BlogPost } from "Domain/Blog.js"
import { UserFromIdResolver } from "Domain/User.js"
import { RepoDefault } from "api/lib/layers.js"
import { Effect } from "effect"
import { Context } from "effect-app"
import { NonEmptyString255, NonEmptyString2k } from "effect-app/Schema"
import { BlogPost } from "models/Blog.js"
import { UserFromIdResolver } from "models/User.js"
import { UserRepo } from "./UserRepo.js"

export type BlogPostSeed = "sample" | ""

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BlogPost } from "models/Blog.js"
import { S } from "resources/lib.js"
import { UserViewFromId } from "../resolvers/UserResolver.js"
import { UserViewFromId } from "api/Accounts/UserResolver.js"
import { BlogPost } from "Domain/Blog.js"
import { S } from "lib/resources.js"

export class BlogPostView extends S.ExtendedClass<BlogPostView, BlogPostView.Encoded>()({
...BlogPost.omit("author"),
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion api/src/resources/Events.ts → api/src/Domain/Events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { S } from "lib/resources.js"
import type { Schema } from "effect-app/Schema"
import { S } from "resources/lib.js"

export class BogusEvent extends S.ExtendedTaggedClass<BogusEvent, BogusEvent.Encoded>()("BogusEvent", {
id: S.StringId.withDefault,
Expand Down
File renamed without changes.
8 changes: 4 additions & 4 deletions api/src/HelloWorld.controllers.ts → api/src/HelloWorld.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { getRequestContext } from "@effect-app/infra/api/setupRequest"
import { generate } from "@effect-app/infra/test"
import { matchFor } from "api/lib/routing.js"
import { UserRepo } from "api/services.js"
import { Effect, S } from "effect-app"
import { User } from "models/User.js"
import { HelloWorldRsc } from "resources.js"
import { HelloWorldApi } from "resources.js"
import { UserRepo } from "./Accounts/UserRepo.js"
import { User } from "./Domain/User.js"

export default matchFor(HelloWorldRsc)([
export default matchFor(HelloWorldApi)([
UserRepo.Default
], ({ GetHelloWorld }) =>
Effect.gen(function*() {
Expand Down
6 changes: 3 additions & 3 deletions api/src/resources/HelloWorld.ts → api/src/HelloWorld/Api.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RequestContext } from "@effect-app/infra/RequestContext"
import { S } from "./lib.js"
import { UserView } from "./views.js"
import { UserView } from "api/Accounts/UserView.js"
import { S } from "lib/resources.js"

class Response extends S.Class<Response>()({
now: S.Date.withDefault,
Expand All @@ -14,6 +14,6 @@ export class GetHelloWorld extends S.Req<GetHelloWorld>()("GetHelloWorld", {
echo: S.String
}, { allowAnonymous: true, allowRoles: ["user"], success: Response }) {}

// codegen:start {preset: meta, sourcePrefix: src/resources/}
//// codegen:start {preset: meta, sourcePrefix: src/HelloWorld/}
export const meta = { moduleName: "HelloWorld" } as const
// codegen:end
14 changes: 0 additions & 14 deletions api/src/Me.controllers.ts

This file was deleted.

6 changes: 3 additions & 3 deletions api/src/Operations.controllers.ts → api/src/Operations.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { OperationsDefault } from "api/lib.js"
import { matchFor } from "api/lib/routing.js"
import { Operations } from "api/services.js"
import { Effect } from "effect-app"
import { OperationsRsc } from "resources.js"
import { OperationsDefault } from "./lib/layers.js"
import { OperationsApi } from "resources.js"

export default matchFor(OperationsRsc)([
export default matchFor(OperationsApi)([
OperationsDefault
], ({ FindOperation }) =>
Effect.gen(function*() {
Expand Down
11 changes: 5 additions & 6 deletions api/src/resources/Operations.ts → api/src/Operations/Api.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { clientFor, S } from "lib/resources.js"
import { Duration, Effect } from "effect-app"
import { NotFoundError } from "effect-app/client"
import { Operation, OperationFailure, OperationId } from "effect-app/Operations"
import { clientFor } from "./lib.js"
import * as S from "./lib/schema.js"

export class FindOperation extends S.Req<FindOperation>()("FindOperation", {
id: OperationId
}, { allowAnonymous: true, allowRoles: ["user"], success: S.NullOr(Operation) }) {}

// codegen:start {preset: meta, sourcePrefix: src/resources/}
//// codegen:start {preset: meta, sourcePrefix: src/Operations/}
export const meta = { moduleName: "Operations" } as const
// codegen:end

// Extensions
const opsClient = clientFor({ FindOperation, meta })
const operationsApi = clientFor({ FindOperation, meta })

export function refreshAndWaitAForOperation<R2, E2, A2>(
refresh: Effect<A2, E2, R2>,
Expand Down Expand Up @@ -73,13 +72,13 @@ const isFailure = S.is(OperationFailure)
function _waitForOperation(id: OperationId, cb?: (op: Operation) => void) {
return Effect
.gen(function*() {
let r = yield* opsClient.FindOperation.handler({ id })
let r = yield* operationsApi.FindOperation.handler({ id })
while (r) {
if (cb) cb(r)
const result = r.result
if (result) return isFailure(result) ? yield* Effect.fail(result) : yield* Effect.succeed(result)
yield* Effect.sleep(Duration.seconds(2))
r = yield* opsClient.FindOperation.handler({ id })
r = yield* operationsApi.FindOperation.handler({ id })
}
return yield* new NotFoundError({ type: "Operation", id })
})
Expand Down
13 changes: 6 additions & 7 deletions api/src/controllers.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// codegen:start {preset: barrel, include: ./*.controllers.ts, import: default}
import blogControllers from "./Blog.controllers.js"
import helloWorldControllers from "./HelloWorld.controllers.js"
import meControllers from "./Me.controllers.js"
import operationsControllers from "./Operations.controllers.js"
import usersControllers from "./Users.controllers.js"
// codegen:start {preset: barrel, include: './[A-Z]*.ts', import: default}
import accounts from "./Accounts.js"
import blog from "./Blog.js"
import helloWorld from "./HelloWorld.js"
import operations from "./Operations.js"

export { blogControllers, helloWorldControllers, meControllers, operationsControllers, usersControllers }
export { accounts, blog, helloWorld, operations }
// codegen:end
8 changes: 8 additions & 0 deletions api/src/lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// codegen:start {preset: barrel, include: ./lib/*.ts, exclude: ./lib/schema.ts}
export * from "./lib/basicRuntime.js"
export * from "./lib/layers.js"
export * from "./lib/middleware.js"
export * from "./lib/observability.js"
export * from "./lib/resources.js"
export * from "./lib/routing.js"
// codegen:end
2 changes: 1 addition & 1 deletion api/src/lib/middleware/events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { makeSSE } from "@effect-app/infra/api/middlewares"
import { ClientEvents } from "Domain/Events.js"
import { Events } from "api/services.js"
import { Effect } from "effect-app"
import { ClientEvents } from "resources.js"

export const makeEvents = Events.pipe(Effect.map((events) => makeSSE(events.stream, ClientEvents)))
2 changes: 2 additions & 0 deletions api/src/lib/resources.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./resources/req.js"
export * as S from "./resources/schema.js"
8 changes: 4 additions & 4 deletions api/src/resources/lib/req.ts → api/src/lib/resources/req.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { NotLoggedInError, UnauthorizedError } from "@effect-app/infra/errors"
import { Duration, Layer, Request as EffectRequest } from "effect-app"
import type { Role } from "Domain/User.js"
import { Duration, Layer, Request } from "effect-app"
import type { RPCContextMap } from "effect-app/client"
import { makeRpcClient } from "effect-app/client"
import type { Role } from "models/User.js"

import { makeClientFor } from "effect-app/client/clientFor"

type CTXMap = {
Expand All @@ -28,9 +27,10 @@ export const { TaggedRequest: Req } = makeRpcClient<RequestConfig, CTXMap>({

export const RequestCacheLayers = Layer.mergeAll(
Layer.setRequestCache(
EffectRequest.makeCache({ capacity: 500, timeToLive: Duration.hours(8) })
Request.makeCache({ capacity: 500, timeToLive: Duration.hours(8) })
),
Layer.setRequestCaching(true),
Layer.setRequestBatching(true)
)

export const clientFor = makeClientFor(RequestCacheLayers)
File renamed without changes.
Loading