Skip to content

Commit 5a74643

Browse files
committed
Add a Publish Post action, and monitor it for Operation progress.
1 parent 6073343 commit 5a74643

File tree

7 files changed

+129
-5
lines changed

7 files changed

+129
-5
lines changed

_project/api/_src/Usecases/Blog.Controllers.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { BlogPostRepo } from "@/services.js"
1+
import { NotFoundError } from "@/errors.js"
2+
import { BlogPostRepo, Operations } from "@/services.js"
23
import { BlogPost } from "@effect-app-boilerplate/models/Blog"
34
import { BlogRsc } from "@effect-app-boilerplate/resources"
5+
import { PositiveInt } from "@effect-app/prelude/schema"
46

57
const { controllers, matchWithServices } = matchFor(BlogRsc)
68

@@ -24,4 +26,47 @@ const CreatePost = matchWithServices("CreatePost")(
2426
.map(_ => _.id)
2527
)
2628

27-
export const BlogControllers = controllers(Effect.struct({ FindPost, GetPosts, CreatePost }))
29+
const PublishPost = matchWithServices("PublishPost")(
30+
{ BlogPostRepo, Operations },
31+
(req, { BlogPostRepo, Operations }) =>
32+
Do($ => {
33+
$(
34+
BlogPostRepo.find(req.id)
35+
.flatMap(_ => _.encaseInEffect(() => new NotFoundError("BlogPost", req.id)))
36+
)
37+
38+
const targets = [
39+
"google",
40+
"twitter",
41+
"facebook"
42+
]
43+
44+
const done: string[] = []
45+
46+
const operationId = $(
47+
Effect.forkOperation(
48+
opId =>
49+
Operations.update(opId, {
50+
total: PositiveInt(targets.length),
51+
completed: PositiveInt(done.length)
52+
}) >
53+
targets
54+
.forEachEffect(_ =>
55+
Effect(done.push(_))
56+
.tap(() =>
57+
Operations.update(opId, {
58+
total: PositiveInt(targets.length),
59+
completed: PositiveInt(done.length)
60+
})
61+
)
62+
.delay(Duration.seconds(4))
63+
)
64+
.map(() => "the answer to the universe is 41")
65+
)
66+
)
67+
68+
return operationId
69+
})
70+
)
71+
72+
export const BlogControllers = controllers(Effect.struct({ FindPost, GetPosts, CreatePost, PublishPost }))

_project/api/_src/services/BlogPostRepo.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { BlogPost, BlogPostId } from "@effect-app-boilerplate/models/Blog"
1+
import { BlogPost, BlogPostId } from "@effect-app-boilerplate/models/Blog"
22

33
export interface BlogPostRepo {
44
all: Effect<never, never, readonly BlogPost[]>
@@ -8,7 +8,13 @@ export interface BlogPostRepo {
88
export const BlogPostRepo = Tag<BlogPostRepo>()
99

1010
export const BlogPostRepoLive = Layer(BlogPostRepo, () => {
11-
const items: BlogPost[] = []
11+
const items: BlogPost[] = [
12+
new BlogPost({
13+
id: BlogPostId("post-test123"),
14+
title: ReasonableString("Test post"),
15+
body: LongString("imma test body")
16+
})
17+
]
1218

1319
return {
1420
all: Effect([...items]),

_project/api/openapi.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,50 @@
172172
}
173173
}
174174
},
175+
"/blog/posts/{id}/publish": {
176+
"post": {
177+
"parameters": [
178+
{
179+
"name": "id",
180+
"in": "path",
181+
"required": true,
182+
"schema": {
183+
"minLength": 6,
184+
"maxLength": 50,
185+
"title": "BlogPostId",
186+
"type": "string"
187+
}
188+
}
189+
],
190+
"requestBody": {
191+
"content": {
192+
"application/json": {
193+
"schema": {
194+
"properties": {},
195+
"type": "object"
196+
}
197+
}
198+
}
199+
},
200+
"responses": {
201+
"200": {
202+
"description": "OK",
203+
"content": {
204+
"application/json": {
205+
"schema": {
206+
"minLength": 6,
207+
"maxLength": 50,
208+
"type": "string"
209+
}
210+
}
211+
}
212+
},
213+
"400": {
214+
"description": "ValidationError"
215+
}
216+
}
217+
}
218+
},
175219
"/hello-world": {
176220
"get": {
177221
"parameters": [],

_project/frontend-nuxt/pages/blog/[id].vue

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,28 @@ import { BlogPostId } from "@effect-app-boilerplate/models/Blog"
55
const { id } = useRouteParams({ id: BlogPostId })
66
77
const blogClient = clientFor(BlogRsc)
8-
const [, latestPost] = useSafeQueryWithArg(blogClient.findPost, { id })
8+
const [, latestPost, reloadPost] = useSafeQueryWithArg(blogClient.findPost, {
9+
id,
10+
})
11+
12+
const progress = ref("")
13+
const [publishing, publish] = useAndHandleMutation(
14+
refreshAndWaitForOperation(
15+
blogClient.publishPost,
16+
Effect.promise(() => reloadPost()),
17+
op => {
18+
progress.value = `${op.progress?.completed}/${op.progress?.total}`
19+
}
20+
),
21+
"Publish Blog Post"
22+
)
923
</script>
1024

1125
<template>
1226
<div v-if="latestPost">
27+
<v-btn @click="publish({ id })" :disabled="publishing.loading">
28+
Publish to all blog sites {{ publishing.loading && `(${progress})` }}
29+
</v-btn>
1330
<div>Title: {{ latestPost.title }}</div>
1431
<div>Body: {{ latestPost.body }}</div>
1532
</div>

_project/resources/_src/Blog.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
export * as CreatePost from "./Blog/CreatePost.js"
33
export * as FindPost from "./Blog/FindPost.js"
44
export * as GetPosts from "./Blog/GetPosts.js"
5+
export * as PublishPost from "./Blog/PublishPost.js"
56
// codegen:end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { BlogPostId } from "@effect-app-boilerplate/models/Blog"
2+
import { OperationId } from "../Views.js"
3+
4+
@allowAnonymous
5+
@allowRoles("user")
6+
export class PublishPostRequest extends Post("/blog/posts/:id/publish")<PublishPostRequest>()({
7+
id: prop(BlogPostId)
8+
}) {}
9+
10+
export const PublishPostResponse = OperationId

_project/resources/_src/Operations/Find.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { nullable } from "@effect-app/prelude/schema"
22
import { Operation, OperationId } from "../Views.js"
33

44
@allowRoles("user")
5+
@allowAnonymous
56
export class FindOperationRequest extends Get("/operations/:id")<FindOperationRequest>()({
67
id: prop(OperationId)
78
}) {}

0 commit comments

Comments
 (0)