diff --git a/api/package.json b/api/package.json index 487f3ba19..03246fe21 100644 --- a/api/package.json +++ b/api/package.json @@ -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": { diff --git a/api/src/Accounts.controllers.ts b/api/src/Accounts.controllers.ts deleted file mode 100644 index 2baeb470f..000000000 --- a/api/src/Accounts.controllers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { matchFor, Router } from "#api/lib/routing" -import { UserRepo } from "#api/services" -import { AccountsRsc } from "#resources" -import { Effect } from "effect-app" - -export default Router(AccountsRsc)({ - dependencies: [UserRepo.Default], - effect: Effect.gen(function*() { - const userRepo = yield* UserRepo - - return matchFor(AccountsRsc)({ - GetMe: userRepo.getCurrentUser - }) - }) -}) diff --git a/api/src/Accounts/GetMe.ts b/api/src/Accounts/GetMe.ts new file mode 100644 index 000000000..5550c1e05 --- /dev/null +++ b/api/src/Accounts/GetMe.ts @@ -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()("Accounts.GetMe", {}, { success: User, failure: NotFoundError }) {} + +export default handlerFor(GetMe)({ + dependencies: [UserRepo.Default], + effect: Effect.gen(function*() { + const userRepo = yield* UserRepo + + return userRepo.getCurrentUser + }) +}) diff --git a/api/src/Accounts/IndexUsers.ts b/api/src/Accounts/IndexUsers.ts new file mode 100644 index 000000000..fa2701302 --- /dev/null +++ b/api/src/Accounts/IndexUsers.ts @@ -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()("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)) + }))) + }) +}) diff --git a/api/src/services/DBContext/UserRepo.ts b/api/src/Accounts/UserRepo.ts similarity index 95% rename from api/src/services/DBContext/UserRepo.ts rename to api/src/Accounts/UserRepo.ts index 6d0e36804..04bc25b02 100644 --- a/api/src/services/DBContext/UserRepo.ts +++ b/api/src/Accounts/UserRepo.ts @@ -1,7 +1,6 @@ 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" @@ -9,8 +8,8 @@ import { Array, Effect, Exit, Layer, Option, pipe, Request, RequestResolver, S } 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" | "" diff --git a/api/src/models/User.ts b/api/src/Accounts/models.ts similarity index 100% rename from api/src/models/User.ts rename to api/src/Accounts/models.ts diff --git a/api/src/resources/resolvers/UserResolver.ts b/api/src/Accounts/resolvers.ts similarity index 90% rename from api/src/resources/resolvers/UserResolver.ts rename to api/src/Accounts/resolvers.ts index 9d6ec15e8..8c5c2ccf1 100644 --- a/api/src/resources/resolvers/UserResolver.ts +++ b/api/src/Accounts/resolvers.ts @@ -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> { readonly _tag: "GetUserViewById" @@ -15,7 +15,7 @@ const GetUserViewById = Request.tagged("GetUserViewById") const getUserViewByIdResolver = RequestResolver .makeBatched((requests: GetUserViewById[]) => - clientFor(UsersRsc).pipe( + clientFor({ IndexUsers }).pipe( Effect.flatMap((client) => client .IndexUsers diff --git a/api/src/resources/views/UserView.ts b/api/src/Accounts/views.ts similarity index 91% rename from api/src/resources/views/UserView.ts rename to api/src/Accounts/views.ts index 928ac6e1b..666ebda07 100644 --- a/api/src/resources/views/UserView.ts +++ b/api/src/Accounts/views.ts @@ -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()({ ...User.pick("id", "role"), diff --git a/api/src/Blog.controllers.ts b/api/src/Blog.controllers.ts deleted file mode 100644 index 7218316fc..000000000 --- a/api/src/Blog.controllers.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { matchFor, Router } from "#api/lib/routing" -import { BlogPostRepo, Events, Operations, UserRepo } from "#api/services" -import { BlogPost } from "#models/Blog" -import { BlogRsc } from "#resources" -import { BogusEvent } from "#resources/Events" -import { Duration, Effect, Schedule } from "effect" -import { Option } from "effect-app" -import { NonEmptyString2k, NonNegativeInt } from "effect-app/Schema" -import { OperationsDefault } from "./lib/layers.js" - -export default Router(BlogRsc)({ - dependencies: [ - BlogPostRepo.Default, - UserRepo.Default, - OperationsDefault, - Events.Default - ], - effect: Effect.gen(function*() { - const blogPostRepo = yield* BlogPostRepo - const userRepo = yield* UserRepo - const events = yield* Events - const operations = yield* Operations - - return matchFor(BlogRsc)({ - FindPost: (req) => - blogPostRepo - .find(req.id) - .pipe(Effect.andThen(Option.getOrNull)), - GetPosts: blogPostRepo - .all - .pipe(Effect.andThen((items) => ({ items }))), - CreatePost: (req) => - userRepo - .getCurrentUser - .pipe( - Effect.andThen((author) => (new BlogPost({ ...req, author }, true))), - Effect.tap(blogPostRepo.save) - ), - PublishPost: (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 - }) - }) - }) -}) diff --git a/api/src/Blog/CreatePost.ts b/api/src/Blog/CreatePost.ts new file mode 100644 index 000000000..87b6759c0 --- /dev/null +++ b/api/src/Blog/CreatePost.ts @@ -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()("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) + ) + }) +}) diff --git a/api/src/Blog/FindPost.ts b/api/src/Blog/FindPost.ts new file mode 100644 index 000000000..afe03e09a --- /dev/null +++ b/api/src/Blog/FindPost.ts @@ -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()("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)) + }) +}) diff --git a/api/src/Blog/ListPosts.ts b/api/src/Blog/ListPosts.ts new file mode 100644 index 000000000..354a1c99a --- /dev/null +++ b/api/src/Blog/ListPosts.ts @@ -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()("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 }))) + }) +}) diff --git a/api/src/Blog/PublishPost.ts b/api/src/Blog/PublishPost.ts new file mode 100644 index 000000000..0438e31c0 --- /dev/null +++ b/api/src/Blog/PublishPost.ts @@ -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()("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 + }) + }) +}) diff --git a/api/src/services/DBContext/BlogPostRepo.ts b/api/src/Blog/Repo.ts similarity index 90% rename from api/src/services/DBContext/BlogPostRepo.ts rename to api/src/Blog/Repo.ts index 36d97a1f5..c5c0d5917 100644 --- a/api/src/services/DBContext/BlogPostRepo.ts +++ b/api/src/Blog/Repo.ts @@ -1,11 +1,11 @@ +import { UserFromIdResolver } from "#Accounts/models" import { RepoDefault } from "#api/lib/layers" -import { BlogPost } from "#models/Blog" -import { UserFromIdResolver } from "#models/User" import { Model } from "@effect-app/infra" import { Effect } from "effect" import { Context } from "effect-app" import { NonEmptyString255, NonEmptyString2k } from "effect-app/Schema" -import { UserRepo } from "./UserRepo.js" +import { UserRepo } from "../Accounts/UserRepo.js" +import { BlogPost } from "./models.js" export type BlogPostSeed = "sample" | "" diff --git a/api/src/models/Blog.ts b/api/src/Blog/models.ts similarity index 94% rename from api/src/models/Blog.ts rename to api/src/Blog/models.ts index ffb31e216..60f854358 100644 --- a/api/src/models/Blog.ts +++ b/api/src/Blog/models.ts @@ -1,5 +1,5 @@ +import { UserFromId } from "#Accounts/models" import { S } from "effect-app" -import { UserFromId } from "./User.js" export const BlogPostId = S.prefixedStringId()("post", "BlogPostId") export interface BlogPostIdBrand { diff --git a/api/src/resources/views/PostView.ts b/api/src/Blog/views.ts similarity index 81% rename from api/src/resources/views/PostView.ts rename to api/src/Blog/views.ts index 7aa56a9c9..b1312a523 100644 --- a/api/src/resources/views/PostView.ts +++ b/api/src/Blog/views.ts @@ -1,6 +1,6 @@ -import { BlogPost } from "#models/Blog" import { S } from "#resources/lib" -import { UserViewFromId } from "../resolvers/UserResolver.js" +import { UserViewFromId } from "../Accounts/resolvers.js" +import { BlogPost } from "./models.js" export class BlogPostView extends S.ExtendedClass()({ ...BlogPost.omit("author"), diff --git a/api/src/HelloWorld.controllers.ts b/api/src/HelloWorld.controllers.ts deleted file mode 100644 index 132b9b96a..000000000 --- a/api/src/HelloWorld.controllers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { matchFor, Router } from "#api/lib/routing" -import { UserRepo } from "#api/services" -import { User } from "#models/User" -import { HelloWorldRsc } from "#resources" -import { GetHelloWorld } from "#resources/HelloWorld" -import { getRequestContext } from "@effect-app/infra/api/setupRequest" -import { generate } from "@effect-app/infra/test" -import { Effect, S } from "effect-app" - -export default Router(HelloWorldRsc)({ - dependencies: [UserRepo.Default], - effect: Effect.gen(function*() { - const userRepo = yield* UserRepo - - return matchFor(HelloWorldRsc)({ - GetHelloWorld: ({ echo }) => - Effect.gen(function*() { - const context = yield* getRequestContext - return yield* userRepo - .tryGetCurrentUser - .pipe( - Effect.catchTags({ - "NotLoggedInError": () => Effect.succeed(null), - "NotFoundError": () => Effect.succeed(null) - }), - Effect.andThen((user) => - new GetHelloWorld.success({ - context, - echo, - currentUser: user, - randomUser: generate(S.A.make(User)).value - }) - ) - ) - }) - }) - }) -}) diff --git a/api/src/HelloWorld/GetHelloWorld.ts b/api/src/HelloWorld/GetHelloWorld.ts new file mode 100644 index 000000000..71cc5e330 --- /dev/null +++ b/api/src/HelloWorld/GetHelloWorld.ts @@ -0,0 +1,49 @@ +import { UserView } from "#Accounts/views" +import { handlerFor } from "#api/lib/handler" +import { S } from "#resources/lib" +import { getRequestContext } from "@effect-app/infra/api/setupRequest" +import { RequestContext } from "@effect-app/infra/RequestContext" +import { generate } from "@effect-app/infra/test" +import { Effect } from "effect-app" +import { User } from "../Accounts/models.js" +import { UserRepo } from "../Accounts/UserRepo.js" + +class Response extends S.Class()({ + now: S.Date.withDefault, + echo: S.String, + context: RequestContext, + currentUser: S.NullOr(UserView), + randomUser: UserView +}) {} + +export class GetHelloWorld extends S.Req()("HelloWorld.GetHelloWorld", { + echo: S.String +}, { allowAnonymous: true, allowRoles: ["user"], success: Response }) {} + +export default handlerFor(GetHelloWorld)({ + dependencies: [UserRepo.Default], + effect: Effect.gen(function*() { + const userRepo = yield* UserRepo + + return ({ echo }) => + Effect.gen(function*() { + const context = yield* getRequestContext + return yield* userRepo + .tryGetCurrentUser + .pipe( + Effect.catchTags({ + "NotLoggedInError": () => Effect.succeed(null), + "NotFoundError": () => Effect.succeed(null) + }), + Effect.andThen((user) => + new GetHelloWorld.success({ + context, + echo, + currentUser: user, + randomUser: generate(S.A.make(User)).value + }) + ) + ) + }) + }) +}) diff --git a/api/src/Operations.controllers.ts b/api/src/Operations.controllers.ts deleted file mode 100644 index 4a22c15f9..000000000 --- a/api/src/Operations.controllers.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { matchFor, Router } from "#api/lib/routing" -import { Operations } from "#api/services" -import { OperationsRsc } from "#resources" -import { Effect } from "effect-app" -import { OperationsDefault } from "./lib/layers.js" - -export default Router(OperationsRsc)({ - dependencies: [OperationsDefault], - effect: Effect.gen(function*() { - const operations = yield* Operations - - return matchFor(OperationsRsc)({ - FindOperation: ({ id }) => - operations - .find(id) - .pipe(Effect.andThen((_) => _.value ?? null)) - }) - }) -}) diff --git a/api/src/Operations/FindOperation.ts b/api/src/Operations/FindOperation.ts new file mode 100644 index 000000000..709d94d24 --- /dev/null +++ b/api/src/Operations/FindOperation.ts @@ -0,0 +1,22 @@ +import { handlerFor } from "#api/lib/handler" +import { OperationsDefault } from "#api/lib/layers" +import { Operations } from "#api/services" +import { S } from "#resources/lib" +import { Effect } from "effect-app" +import { Operation, OperationId } from "effect-app/Operations" + +export class FindOperation extends S.Req()("Operations.FindOperation", { + id: OperationId +}, { allowAnonymous: true, allowRoles: ["user"], success: S.NullOr(Operation) }) {} + +export default handlerFor(FindOperation)({ + dependencies: [OperationsDefault], + effect: Effect.gen(function*() { + const operations = yield* Operations + + return ({ id }) => + operations + .find(id) + .pipe(Effect.andThen((_) => _.value ?? null)) + }) +}) diff --git a/api/src/Users.controllers.ts b/api/src/Users.controllers.ts deleted file mode 100644 index 7bc562689..000000000 --- a/api/src/Users.controllers.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { matchFor, Router } from "#api/lib/routing" -import { Q, UserRepo } from "#api/services" -import { UsersRsc } from "#resources" -import type { UserView } from "#resources/views" -import { Array } from "effect" -import { Effect, Order } from "effect-app" - -export default Router(UsersRsc)({ - dependencies: [UserRepo.Default], - effect: Effect.gen(function*() { - const userRepo = yield* UserRepo - - return matchFor(UsersRsc)({ - IndexUsers: (req) => - userRepo - .query(Q.where("id", "in", req.filterByIds)) - .pipe(Effect.andThen((users) => ({ - users: Array.sort(users, Order.mapInput(Order.string, (_: UserView) => _.displayName)) - }))) - }) - }) -}) diff --git a/api/src/controllers.ts b/api/src/controllers.ts index 0d5106915..7746db3c5 100644 --- a/api/src/controllers.ts +++ b/api/src/controllers.ts @@ -1,9 +1,5 @@ -// codegen:start {preset: barrel, include: ./*.controllers.ts, import: default} -import accountsControllers from "./Accounts.controllers.js" -import blogControllers from "./Blog.controllers.js" -import helloWorldControllers from "./HelloWorld.controllers.js" -import operationsControllers from "./Operations.controllers.js" -import usersControllers from "./Users.controllers.js" +// TODO -export { accountsControllers, blogControllers, helloWorldControllers, operationsControllers, usersControllers } +// codegen:start {preset: barrel, include: ./*.controllers.ts, import: default} +export {} // codegen:end diff --git a/api/src/lib/handler.ts b/api/src/lib/handler.ts new file mode 100644 index 000000000..4b18352ee --- /dev/null +++ b/api/src/lib/handler.ts @@ -0,0 +1,21 @@ +import type { Effect, Layer, S } from "effect-app" +import type { TaggedRequestClassAny } from "effect-app/client" + +// TODO +export const handlerFor: ( + r: Req +) => { + // TODO: constrain L to be strict; only layers that are used in effect, or the other way around + ( + hndlr: { + dependencies?: L + effect: Effect<(req: S.Schema.Type) => Effect, E2, R2>, E, R> + } + ): typeof hndlr + ( + hndlr: { + dependencies?: L + effect: Effect, E2, R2>, E, R> + } + ): typeof hndlr +} = (_) => (i) => i // TODO: as Layer? diff --git a/api/src/resources/Accounts.ts b/api/src/resources/Accounts.ts index 80068cafb..c52d143cf 100644 --- a/api/src/resources/Accounts.ts +++ b/api/src/resources/Accounts.ts @@ -1,8 +1,6 @@ -import { User } from "#models/User" -import { NotFoundError } from "effect-app/client" -import { S } from "./lib.js" - -export class GetMe extends S.Req()("GetMe", {}, { success: User, failure: NotFoundError }) {} +//// codegen:start {preset: barrel, include: ../Blog/*.ts, export: { as: 'PascalCase' }} +export { GetMe } from "../Accounts/GetMe.js" +//// codegen:end // codegen:start {preset: meta, sourcePrefix: src/resources/} export const meta = { moduleName: "Accounts" } as const diff --git a/api/src/resources/Blog.ts b/api/src/resources/Blog.ts index 843cba4a4..d84fb27d3 100644 --- a/api/src/resources/Blog.ts +++ b/api/src/resources/Blog.ts @@ -1,30 +1,9 @@ -import { BlogPost, BlogPostId } from "#models/Blog" -import { InvalidStateError, NotFoundError, OptimisticConcurrencyException } from "effect-app/client" -import { OperationId } from "effect-app/Operations" -import { S } from "./lib.js" -import { BlogPostView } from "./views.js" - -export class CreatePost extends S.Req()("CreatePost", BlogPost.pick("title", "body"), { - allowRoles: ["user"], - success: S.Struct({ id: BlogPostId }), - failure: S.Union(NotFoundError, InvalidStateError, OptimisticConcurrencyException) -}) {} - -export class FindPost extends S.Req()("FindPost", { - id: BlogPostId -}, { allowAnonymous: true, allowRoles: ["user"], success: S.NullOr(BlogPostView) }) {} - -export class GetPosts extends S.Req()("GetPosts", {}, { - allowAnonymous: true, - allowRoles: ["user"], - success: S.Struct({ - items: S.Array(BlogPostView) - }) -}) {} - -export class PublishPost extends S.Req()("PublishPost", { - id: BlogPostId -}, { allowRoles: ["user"], success: OperationId, failure: S.Union(NotFoundError) }) {} +//// codegen:start {preset: barrel, include: ../Blog/*.ts, export: { as: 'PascalCase' }} +export { CreatePost } from "../Blog/CreatePost.js" +export { FindPost } from "../Blog/FindPost.js" +export { ListPosts } from "../Blog/ListPosts.js" +export { PublishPost } from "../Blog/PublishPost.js" +//// codegen:end // codegen:start {preset: meta, sourcePrefix: src/resources/} export const meta = { moduleName: "Blog" } as const diff --git a/api/src/resources/HelloWorld.ts b/api/src/resources/HelloWorld.ts index 3f5cb5211..ab6ae8abe 100644 --- a/api/src/resources/HelloWorld.ts +++ b/api/src/resources/HelloWorld.ts @@ -1,18 +1,4 @@ -import { RequestContext } from "@effect-app/infra/RequestContext" -import { S } from "./lib.js" -import { UserView } from "./views.js" - -class Response extends S.Class()({ - now: S.Date.withDefault, - echo: S.String, - context: RequestContext, - currentUser: S.NullOr(UserView), - randomUser: UserView -}) {} - -export class GetHelloWorld extends S.Req()("GetHelloWorld", { - echo: S.String -}, { allowAnonymous: true, allowRoles: ["user"], success: Response }) {} +export { GetHelloWorld } from "../HelloWorld/GetHelloWorld.js" // codegen:start {preset: meta, sourcePrefix: src/resources/} export const meta = { moduleName: "HelloWorld" } as const diff --git a/api/src/resources/Operations.ts b/api/src/resources/Operations.ts index 0b38c13d5..3cef8b0a6 100644 --- a/api/src/resources/Operations.ts +++ b/api/src/resources/Operations.ts @@ -1,12 +1,12 @@ import { Duration, Effect } from "effect-app" import { NotFoundError } from "effect-app/client" -import { Operation, OperationFailure, OperationId } from "effect-app/Operations" +import type { Operation, OperationId } from "effect-app/Operations" +import { OperationFailure } from "effect-app/Operations" +import { FindOperation } from "../Operations/FindOperation.js" import { clientFor } from "./lib.js" import * as S from "./lib/schema.js" -export class FindOperation extends S.Req()("FindOperation", { - id: OperationId -}, { allowAnonymous: true, allowRoles: ["user"], success: S.NullOr(Operation) }) {} +export { FindOperation } from "../Operations/FindOperation.js" // codegen:start {preset: meta, sourcePrefix: src/resources/} export const meta = { moduleName: "Operations" } as const diff --git a/api/src/resources/Users.ts b/api/src/resources/Users.ts index c73252369..3cfb99aad 100644 --- a/api/src/resources/Users.ts +++ b/api/src/resources/Users.ts @@ -1,16 +1,4 @@ -import { UserId } from "#models/User" -import { S } from "./lib.js" -import { UserView } from "./views/UserView.js" - -export class IndexUsers extends S.Req()("IndexUsers", { - filterByIds: S.NonEmptyArray(UserId) -}, { - allowAnonymous: true, - allowRoles: ["user"], - success: S.Struct({ - users: S.Array(UserView) - }) -}) {} +export { IndexUsers } from "../Accounts/IndexUsers.js" // codegen:start {preset: meta, sourcePrefix: src/resources/} export const meta = { moduleName: "Users" } as const diff --git a/api/src/resources/lib/req.ts b/api/src/resources/lib/req.ts index 5e2e2ae4a..da33d3524 100644 --- a/api/src/resources/lib/req.ts +++ b/api/src/resources/lib/req.ts @@ -1,4 +1,4 @@ -import type { Role } from "#models/User" +import type { Role } from "#Accounts/models" import { NotLoggedInError, UnauthorizedError } from "@effect-app/infra/errors" import { Duration, Layer, Request as EffectRequest } from "effect-app" import type { RPCContextMap } from "effect-app/client" diff --git a/api/src/resources/views.ts b/api/src/resources/views.ts deleted file mode 100644 index b09859be2..000000000 --- a/api/src/resources/views.ts +++ /dev/null @@ -1,4 +0,0 @@ -// codegen:start {preset: barrel, include: ./views/*.ts} -export * from "./views/PostView.js" -export * from "./views/UserView.js" -// codegen:end diff --git a/api/src/services.ts b/api/src/services.ts index eb74b1bc2..692fe6fb7 100644 --- a/api/src/services.ts +++ b/api/src/services.ts @@ -1,5 +1,4 @@ // codegen:start {preset: barrel, include: services/*.ts } -export * from "./services/DBContext.js" export * from "./services/Events.js" export * from "./services/lib.js" export * from "./services/UserProfile.js" diff --git a/api/src/services/DBContext.ts b/api/src/services/DBContext.ts deleted file mode 100644 index f3dec88d7..000000000 --- a/api/src/services/DBContext.ts +++ /dev/null @@ -1,4 +0,0 @@ -// codegen:start {preset: barrel, include: ./DBContext/* } -export * from "./DBContext/BlogPostRepo.js" -export * from "./DBContext/UserRepo.js" -// codegen:end diff --git a/api/src/services/UserProfile.ts b/api/src/services/UserProfile.ts index 18ae6a117..9acf548f0 100644 --- a/api/src/services/UserProfile.ts +++ b/api/src/services/UserProfile.ts @@ -1,4 +1,4 @@ -import { Role } from "#models/User" +import { Role } from "#Accounts/models" import { parseJwt } from "@effect-app/infra/api/routing/schema/jwt" import { Context, S } from "effect-app" import { UserProfileId } from "effect-app/ids" diff --git a/api/tsconfig.json b/api/tsconfig.json index 534c4c0a4..7dd7e922e 100644 --- a/api/tsconfig.json +++ b/api/tsconfig.json @@ -10,6 +10,12 @@ "#models": [ "./src/models.js" ], + "#Accounts/*": [ + "./src/Accounts/*.js" + ], + "#Blog/*": [ + "./src/Blog/*.js" + ], "#resources/*": [ "./src/resources/*.js" ], diff --git a/frontend/composables/currentUser.ts b/frontend/composables/currentUser.ts index 651474d4e..662428dae 100644 --- a/frontend/composables/currentUser.ts +++ b/frontend/composables/currentUser.ts @@ -1,6 +1,6 @@ // Naive login, good enough for the start -import type { UserId } from "#models/User" +import type { UserId } from "#Accounts/models" export function getUserId() { return useCookie("user-id") diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 921a92b54..69f188cf9 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -24,10 +24,15 @@ export default defineNuxtConfig({ }, alias: { - "#resources": fileURLToPath( - new URL("../api/src/resources", import.meta.url), + ...["Accounts", "Blog", "resources", "models"].reduce( + (prev, cur) => { + prev[`#${cur}`] = fileURLToPath( + new URL("../api/src/" + cur, import.meta.url), + ) + return prev + }, + {} as Record, ), - "#models": fileURLToPath(new URL("../api/src/models", import.meta.url)), ...(localLibs ? { "effect-app": fileURLToPath( diff --git a/frontend/pages/blog/[id].vue b/frontend/pages/blog/[id].vue index 8205b2010..17b882ba0 100644 --- a/frontend/pages/blog/[id].vue +++ b/frontend/pages/blog/[id].vue @@ -1,7 +1,7 @@