Skip to content
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
20 changes: 20 additions & 0 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,26 @@
"types": "./dist/resources/*.d.ts",
"default": "./_cjs/resources/*.cjs"
}
},
"#Accounts/*": {
"import": {
"types": "./dist/Accounts/*.d.ts",
"default": "./dist/Accounts/*.js"
},
"require": {
"types": "./dist/Accounts/*.d.ts",
"default": "./_cjs/Accounts/*.cjs"
}
},
"#Blog/*": {
"import": {
"types": "./dist/Blog/*.d.ts",
"default": "./dist/Blog/*.js"
},
"require": {
"types": "./dist/Blog/*.d.ts",
"default": "./_cjs/Blog/*.cjs"
}
}
},
"dependencies": {
Expand Down
15 changes: 0 additions & 15 deletions api/src/Accounts.controllers.ts

This file was deleted.

17 changes: 17 additions & 0 deletions api/src/Accounts/GetMe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { handlerFor } from "#api/lib/handler"
import { S } from "#resources/lib"
import { Effect } from "effect-app"
import { NotFoundError } from "effect-app/client"
import { User } from "./models.js"
import { UserRepo } from "./UserRepo.js"

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

export default handlerFor(GetMe)({
dependencies: [UserRepo.Default],
effect: Effect.gen(function*() {
const userRepo = yield* UserRepo

return userRepo.getCurrentUser
})
})
31 changes: 31 additions & 0 deletions api/src/Accounts/IndexUsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { handlerFor } from "#api/lib/handler"
import { Q } from "#api/services"
import { S } from "#resources/lib"
import { Array, Effect, Order } from "effect-app"
import { UserId } from "./models.js"
import { UserRepo } from "./UserRepo.js"
import { UserView } from "./views.js"

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

export default handlerFor(IndexUsers)({
dependencies: [UserRepo.Default],
effect: Effect.gen(function*() {
const userRepo = yield* UserRepo

return (req) =>
userRepo
.query(Q.where("id", "in", req.filterByIds))
.pipe(Effect.andThen((users) => ({
users: Array.sort(users, Order.mapInput(Order.string, (_: UserView) => _.displayName))
})))
})
})
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { RepoConfig } from "#api/config"
import { RepoDefault } from "#api/lib/layers"
import type { UserId } from "#models/User"
import { User } from "#models/User"
import { Q, UserProfile } from "#api/services"
import { Model } from "@effect-app/infra"
import { NotFoundError, NotLoggedInError } from "@effect-app/infra/errors"
import { generate } from "@effect-app/infra/test"
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 { Q } from "../lib.js"
import { UserProfile } from "../UserProfile.js"
import type { UserId } from "./models.js"
import { User } from "./models.js"

export type UserSeed = "sample" | ""

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { UserId } from "#models/User"
import { UserId } from "#Accounts/models"
import { UserView } from "#Accounts/views"
import { clientFor } from "#resources/lib"
import { Effect, Exit, Request, RequestResolver } from "effect"
import { Array, Option, pipe, S } from "effect-app"
import { ApiClientFactory, NotFoundError } from "effect-app/client"
import { type Schema } from "effect-app/Schema"
import * as UsersRsc from "../Users.js"
import { UserView } from "../views/UserView.js"
import { IndexUsers } from "./IndexUsers.js"

interface GetUserViewById extends Request.Request<UserView, NotFoundError<"User">> {
readonly _tag: "GetUserViewById"
Expand All @@ -15,7 +15,7 @@ const GetUserViewById = Request.tagged<GetUserViewById>("GetUserViewById")

const getUserViewByIdResolver = RequestResolver
.makeBatched((requests: GetUserViewById[]) =>
clientFor(UsersRsc).pipe(
clientFor({ IndexUsers }).pipe(
Effect.flatMap((client) =>
client
.IndexUsers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { User } from "#models/User"
import { S } from "#resources/lib"
import { User } from "./models.js"

export class UserView extends S.ExtendedClass<UserView, UserView.Encoded>()({
...User.pick("id", "role"),
Expand Down
87 changes: 0 additions & 87 deletions api/src/Blog.controllers.ts

This file was deleted.

32 changes: 32 additions & 0 deletions api/src/Blog/CreatePost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { UserRepo } from "#Accounts/UserRepo"
import { handlerFor } from "#api/lib/handler"
import { S } from "#resources/lib"
import { Effect } from "effect-app"
import { InvalidStateError, NotFoundError, OptimisticConcurrencyException } from "effect-app/client"
import { BlogPost, BlogPostId } from "./models.js"
import { BlogPostRepo } from "./Repo.js"

export class CreatePost extends S.Req<CreatePost>()("Blog.Create", BlogPost.pick("title", "body"), {
allowRoles: ["user"],
success: S.Struct({ id: BlogPostId }),
failure: S.Union(NotFoundError, InvalidStateError, OptimisticConcurrencyException)
}) {}

export default handlerFor(CreatePost)({
dependencies: [
BlogPostRepo.Default,
UserRepo.Default
],
effect: Effect.gen(function*() {
const blogPostRepo = yield* BlogPostRepo
const userRepo = yield* UserRepo

return (req) =>
userRepo
.getCurrentUser
.pipe(
Effect.andThen((author) => (new BlogPost({ ...req, author }, true))),
Effect.tap(blogPostRepo.save)
)
})
})
29 changes: 29 additions & 0 deletions api/src/Blog/FindPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { handlerFor } from "#api/lib/handler"
import { S } from "#resources/lib"
import { Effect } from "effect"
import { Option } from "effect-app"
import { BlogPostId } from "./models.js"
import { BlogPostRepo } from "./Repo.js"
import { BlogPostView } from "./views.js"

export class FindPost extends S.Req<FindPost>()("Blog.FindPost", {
id: BlogPostId
}, {
allowAnonymous: true,
allowRoles: ["user"],
success: S.NullOr(BlogPostView)
}) {}

export default handlerFor(FindPost)({
dependencies: [
BlogPostRepo.Default
],
effect: Effect.gen(function*() {
const blogPostRepo = yield* BlogPostRepo

return (req) =>
blogPostRepo
.find(req.id)
.pipe(Effect.andThen(Option.getOrNull))
})
})
26 changes: 26 additions & 0 deletions api/src/Blog/ListPosts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { handlerFor } from "#api/lib/handler"
import { S } from "#resources/lib"
import { Effect } from "effect-app"
import { BlogPostRepo } from "./Repo.js"
import { BlogPostView } from "./views.js"

export class ListPosts extends S.Req<ListPosts>()("Blog.List", {}, {
allowAnonymous: true,
allowRoles: ["user"],
success: S.Struct({
items: S.Array(BlogPostView)
})
}) {}

export default handlerFor(ListPosts)({
dependencies: [
BlogPostRepo.Default
],
effect: Effect.gen(function*() {
const blogPostRepo = yield* BlogPostRepo

return blogPostRepo
.all
.pipe(Effect.andThen((items) => ({ items })))
})
})
75 changes: 75 additions & 0 deletions api/src/Blog/PublishPost.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { handlerFor } from "#api/lib/handler"
import { OperationsDefault } from "#api/lib/layers"
import { Events, Operations } from "#api/services"
import { BogusEvent } from "#resources/Events"
import { S } from "#resources/lib"
import { Duration, Effect, Schedule } from "effect-app"
import { NotFoundError } from "effect-app/client"
import { OperationId } from "effect-app/Operations"
import { NonEmptyString2k, NonNegativeInt } from "effect-app/Schema"
import { BlogPostId } from "./models.js"
import { BlogPostRepo } from "./Repo.js"

export class PublishPost extends S.Req<PublishPost>()("Blog.PublishPost", {
id: BlogPostId
}, { allowRoles: ["user"], success: OperationId, failure: S.Union(NotFoundError) }) {}

export default handlerFor(PublishPost)({
dependencies: [
BlogPostRepo.Default,
OperationsDefault,
Events.Default
],
effect: Effect.gen(function*() {
const blogPostRepo = yield* BlogPostRepo
const operations = yield* Operations
const events = yield* Events

return (req) =>
Effect.gen(function*() {
const post = yield* blogPostRepo.get(req.id)

console.log("publishing post", post)

const targets = [
"google",
"twitter",
"facebook"
]

const done: string[] = []

const op = yield* operations.fork(
(opId) =>
operations
.update(opId, {
total: NonNegativeInt(targets.length),
completed: NonNegativeInt(done.length)
})
.pipe(
Effect.andThen(Effect.forEach(targets, (_) =>
Effect
.sync(() => done.push(_))
.pipe(
Effect.tap(() =>
operations.update(opId, {
total: NonNegativeInt(targets.length),
completed: NonNegativeInt(done.length)
})
),
Effect.delay(Duration.seconds(4))
))),
Effect.andThen(() => "the answer to the universe is 41")
),
// while operation is running...
(_opId) =>
Effect
.suspend(() => events.publish(new BogusEvent()))
.pipe(Effect.schedule(Schedule.spaced(Duration.seconds(1)))),
NonEmptyString2k("post publishing")
)

return op.id
})
})
})
Loading