From ef4bf56b5266e6f5cb69c9c2f33bc08b57f942fb Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Wed, 18 Jan 2023 12:04:56 +0100 Subject: [PATCH 01/16] Add BlogPost types, client resource and basic frontend Co-authored-by: Enrico Polanski --- _project/frontend-nuxt/pages/blog.vue | 34 ++++++++++++++++++++++ _project/models/_src/Blog.ts | 32 ++++++++++++++++++++ _project/models/package.json | 10 +++++++ _project/resources/_src/Blog.ts | 4 +++ _project/resources/_src/Blog/CreatePost.ts | 7 +++++ _project/resources/_src/Blog/GetPosts.ts | 9 ++++++ _project/resources/_src/index.ts | 1 + 7 files changed, 97 insertions(+) create mode 100644 _project/frontend-nuxt/pages/blog.vue create mode 100644 _project/models/_src/Blog.ts create mode 100644 _project/resources/_src/Blog.ts create mode 100644 _project/resources/_src/Blog/CreatePost.ts create mode 100644 _project/resources/_src/Blog/GetPosts.ts diff --git a/_project/frontend-nuxt/pages/blog.vue b/_project/frontend-nuxt/pages/blog.vue new file mode 100644 index 000000000..fa98c3770 --- /dev/null +++ b/_project/frontend-nuxt/pages/blog.vue @@ -0,0 +1,34 @@ + + + \ No newline at end of file diff --git a/_project/models/_src/Blog.ts b/_project/models/_src/Blog.ts new file mode 100644 index 000000000..4289bdfc4 --- /dev/null +++ b/_project/models/_src/Blog.ts @@ -0,0 +1,32 @@ +import { prefixedStringId } from "@effect-app/prelude/schema" + +export const BlogPostId = prefixedStringId()("post", "BlogPostId") +export interface BlogPostIdBrand { + readonly BlogPostId: unique symbol +} +export type BlogPostId = StringId & BlogPostIdBrand & `post-${string}` + +@useClassFeaturesForSchema +export class BlogPost extends MNModel()({ + id: BlogPostId.withDefault, + title: ReasonableString, + body: LongString, + createdAt: date.withDefault +}) {} + +// codegen:start {preset: model} +// +/* eslint-disable */ +export namespace BlogPost { + /** + * @tsplus type BlogPost.Encoded + * @tsplus companion BlogPost.Encoded/Ops + */ + export class Encoded extends EncodedClass() {} + export interface ConstructorInput + extends ConstructorInputFromApi {} + export interface Props extends GetProvidedProps {} +} +/* eslint-enable */ +// +// codegen:end diff --git a/_project/models/package.json b/_project/models/package.json index 2dfd8bb41..008bdd1f5 100644 --- a/_project/models/package.json +++ b/_project/models/package.json @@ -24,6 +24,16 @@ } }, "exports": { + "./Blog": { + "import": { + "types": "./dist/Blog.d.ts", + "default": "./dist/Blog.js" + }, + "require": { + "types": "./dist/Blog.d.ts", + "default": "./_cjs/Blog.cjs" + } + }, "./User": { "import": { "types": "./dist/User.d.ts", diff --git a/_project/resources/_src/Blog.ts b/_project/resources/_src/Blog.ts new file mode 100644 index 000000000..cbbb430be --- /dev/null +++ b/_project/resources/_src/Blog.ts @@ -0,0 +1,4 @@ +// codegen:start {preset: barrel, include: ./Blog/*.ts, export: { as: 'PascalCase' }, nodir: false } +export * as CreatePost from "./Blog/CreatePost.js" +export * as GetPosts from "./Blog/GetPosts.js" +// codegen:end diff --git a/_project/resources/_src/Blog/CreatePost.ts b/_project/resources/_src/Blog/CreatePost.ts new file mode 100644 index 000000000..7215d03e4 --- /dev/null +++ b/_project/resources/_src/Blog/CreatePost.ts @@ -0,0 +1,7 @@ +import { BlogPost, BlogPostId } from "@effect-app-boilerplate/models/Blog" + +export class CreatePostRequest extends Post("/blog/posts")()( + BlogPost.pick("title", "body") +) {} + +export const CreatePostResponse = BlogPostId diff --git a/_project/resources/_src/Blog/GetPosts.ts b/_project/resources/_src/Blog/GetPosts.ts new file mode 100644 index 000000000..80997f6cc --- /dev/null +++ b/_project/resources/_src/Blog/GetPosts.ts @@ -0,0 +1,9 @@ +import { BlogPost } from "@effect-app-boilerplate/models/Blog" + +export class GetPostsRequest extends Get("/blog/posts")()( + {} +) {} + +export class GetPostsResponse extends Model()({ + items: array(BlogPost) +}) {} diff --git a/_project/resources/_src/index.ts b/_project/resources/_src/index.ts index f87073585..39e2dae54 100644 --- a/_project/resources/_src/index.ts +++ b/_project/resources/_src/index.ts @@ -3,6 +3,7 @@ import "./lib/operations.js" export { ClientEvents } from "./Events.js" // codegen:start {preset: barrel, include: ./*.ts, exclude: [./_global*.ts, ./index.ts, ./lib.ts, ./Views.ts, ./errors.ts], export: { as: 'PascalCase', postfix: 'Rsc' }} +export * as BlogRsc from "./Blog.js" export * as EventsRsc from "./Events.js" export * as HelloWorldRsc from "./HelloWorld.js" export * as MeRsc from "./Me.js" From 5b784f374742ddf09ad75dab05855dd38bd25765 Mon Sep 17 00:00:00 2001 From: Patrick Roza Date: Wed, 18 Jan 2023 12:15:24 +0100 Subject: [PATCH 02/16] Added CreatePost and GetPosts implementation in the Controllers. --- _project/api/_src/Usecases.ts | 3 +- .../api/_src/Usecases/Blog.Controllers.ts | 15 +++ _project/api/openapi.json | 109 ++++++++++++++++++ _project/frontend-nuxt/layouts/default.vue | 2 + _project/frontend-nuxt/pages/blog.vue | 6 +- _project/resources/_src/Blog/CreatePost.ts | 2 + _project/resources/_src/Blog/GetPosts.ts | 2 + 7 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 _project/api/_src/Usecases/Blog.Controllers.ts diff --git a/_project/api/_src/Usecases.ts b/_project/api/_src/Usecases.ts index c723cae25..82bd6af82 100644 --- a/_project/api/_src/Usecases.ts +++ b/_project/api/_src/Usecases.ts @@ -1,7 +1,8 @@ // codegen:start {preset: barrel, include: ./Usecases/*.ts, import: default} +import usecasesBlogControllers from "./Usecases/Blog.Controllers.js" import usecasesHelloWorldControllers from "./Usecases/HelloWorld.Controllers.js" import usecasesMeControllers from "./Usecases/Me.Controllers.js" import usecasesOperationsControllers from "./Usecases/Operations.Controllers.js" -export { usecasesHelloWorldControllers, usecasesMeControllers, usecasesOperationsControllers } +export { usecasesBlogControllers, usecasesHelloWorldControllers, usecasesMeControllers, usecasesOperationsControllers } // codegen:end diff --git a/_project/api/_src/Usecases/Blog.Controllers.ts b/_project/api/_src/Usecases/Blog.Controllers.ts new file mode 100644 index 000000000..1c752d84f --- /dev/null +++ b/_project/api/_src/Usecases/Blog.Controllers.ts @@ -0,0 +1,15 @@ +import { BlogPost } from "@effect-app-boilerplate/models/Blog" +import { BlogRsc } from "@effect-app-boilerplate/resources" + +const items: BlogPost[] = [] + +const blog = matchFor(BlogRsc) + +const GetPosts = blog.matchGetPosts({}, () => Effect({ items })) + +const CreatePost = blog.matchCreatePost({}, (req) => + Effect(new BlogPost({ ...req })) + .tap((post) => Effect(items.push(post))) + .map((_) => _.id)) + +export default blog.controllers({ GetPosts, CreatePost }) diff --git a/_project/api/openapi.json b/_project/api/openapi.json index 4b4669499..395cdf950 100644 --- a/_project/api/openapi.json +++ b/_project/api/openapi.json @@ -6,6 +6,83 @@ }, "tags": [], "paths": { + "/blog/posts": { + "post": { + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "properties": { + "title": { + "minLength": 1, + "maxLength": 255, + "type": "string" + }, + "body": { + "minLength": 1, + "maxLength": 2047, + "type": "string" + } + }, + "required": [ + "title", + "body" + ], + "type": "object" + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "minLength": 6, + "maxLength": 50, + "title": "BlogPostId", + "type": "string" + } + } + } + }, + "400": { + "description": "ValidationError" + } + } + }, + "get": { + "parameters": [], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/BlogPost" + }, + "type": "array" + } + }, + "required": [ + "items" + ], + "type": "object" + } + } + } + }, + "400": { + "description": "ValidationError" + } + } + } + }, "/hello-world": { "get": { "parameters": [], @@ -135,6 +212,38 @@ } }, "schemas": { + "BlogPost": { + "title": "BlogPost", + "properties": { + "id": { + "minLength": 6, + "maxLength": 50, + "title": "BlogPostId", + "type": "string" + }, + "title": { + "minLength": 1, + "maxLength": 255, + "type": "string" + }, + "body": { + "minLength": 1, + "maxLength": 2047, + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + } + }, + "required": [ + "id", + "title", + "body", + "createdAt" + ], + "type": "object" + }, "User": { "title": "User", "properties": { diff --git a/_project/frontend-nuxt/layouts/default.vue b/_project/frontend-nuxt/layouts/default.vue index 17ff2116b..31c21401b 100644 --- a/_project/frontend-nuxt/layouts/default.vue +++ b/_project/frontend-nuxt/layouts/default.vue @@ -31,6 +31,8 @@ onMounted(() => { + | + Blog
{{ router.currentRoute.value.name }}
diff --git a/_project/frontend-nuxt/pages/blog.vue b/_project/frontend-nuxt/pages/blog.vue index fa98c3770..e1e28aef9 100644 --- a/_project/frontend-nuxt/pages/blog.vue +++ b/_project/frontend-nuxt/pages/blog.vue @@ -3,8 +3,10 @@ import { BlogRsc } from "@effect-app-boilerplate/resources" const blogClient = clientFor(BlogRsc) -const [, createPost] = useMutation(blogClient.createPost) -const [, latestPosts] = useSafeQuery(blogClient.getPosts) +const [, createPost_] = useMutation(blogClient.createPost) +const [, latestPosts, reloadPosts] = useSafeQuery(blogClient.getPosts) + +const createPost = flow(createPost_, _ => _.then(_ => reloadPosts()))