From 5aadbbdc2e1864f8555a6ead74697ec5439714e6 Mon Sep 17 00:00:00 2001 From: Maxwell Brown Date: Wed, 11 Sep 2024 16:13:52 -0400 Subject: [PATCH] Add example content to monorepo template (#34) --- templates/monorepo/packages/cli/package.json | 7 +++ templates/monorepo/packages/cli/src/Cli.ts | 39 +++++++++++++ .../monorepo/packages/cli/src/TodosClient.ts | 47 ++++++++++++++++ templates/monorepo/packages/cli/src/bin.ts | 16 ++++++ .../monorepo/packages/cli/tsconfig.build.json | 3 + .../monorepo/packages/cli/tsconfig.src.json | 3 + .../monorepo/packages/cli/tsconfig.test.json | 3 +- .../monorepo/packages/domain/package.json | 1 + .../monorepo/packages/domain/src/Todo.ts | 20 ------- .../monorepo/packages/domain/src/TodosApi.ts | 56 +++++++++++++++++++ .../monorepo/packages/server/package.json | 6 ++ templates/monorepo/packages/server/src/Api.ts | 20 +++++++ .../packages/server/src/TodosRepository.ts | 53 ++++++++++++++++++ .../monorepo/packages/server/src/server.ts | 16 ++++++ .../packages/server/tsconfig.build.json | 3 + .../packages/server/tsconfig.src.json | 3 + .../packages/server/tsconfig.test.json | 3 +- 17 files changed, 277 insertions(+), 22 deletions(-) create mode 100644 templates/monorepo/packages/cli/src/Cli.ts create mode 100644 templates/monorepo/packages/cli/src/TodosClient.ts create mode 100644 templates/monorepo/packages/cli/src/bin.ts delete mode 100644 templates/monorepo/packages/domain/src/Todo.ts create mode 100644 templates/monorepo/packages/domain/src/TodosApi.ts create mode 100644 templates/monorepo/packages/server/src/Api.ts create mode 100644 templates/monorepo/packages/server/src/TodosRepository.ts create mode 100644 templates/monorepo/packages/server/src/server.ts diff --git a/templates/monorepo/packages/cli/package.json b/templates/monorepo/packages/cli/package.json index e927583..6936652 100644 --- a/templates/monorepo/packages/cli/package.json +++ b/templates/monorepo/packages/cli/package.json @@ -12,6 +12,13 @@ "test": "vitest", "coverage": "vitest --coverage" }, + "dependencies": { + "@effect/cli": "latest", + "@effect/platform": "latest", + "@effect/platform-node": "latest", + "@template/domain": "workspace:^", + "effect": "latest" + }, "effect": { "generateExports": { "include": [ diff --git a/templates/monorepo/packages/cli/src/Cli.ts b/templates/monorepo/packages/cli/src/Cli.ts new file mode 100644 index 0000000..943529e --- /dev/null +++ b/templates/monorepo/packages/cli/src/Cli.ts @@ -0,0 +1,39 @@ +import { Args, Command, Options } from "@effect/cli" +import { TodosClient } from "./TodosClient.js" + +const todoArg = Args.text({ name: "todo" }).pipe( + Args.withDescription("The message associated with a todo") +) + +const todoId = Options.integer("id").pipe( + Options.withDescription("The identifier of the todo") +) + +const add = Command.make("add", { todo: todoArg }).pipe( + Command.withDescription("Add a new todo"), + Command.withHandler(({ todo }) => TodosClient.create(todo)) +) + +const done = Command.make("done", { id: todoId }).pipe( + Command.withDescription("Mark a todo as done"), + Command.withHandler(({ id }) => TodosClient.complete(id)) +) + +const list = Command.make("list").pipe( + Command.withDescription("List all todos"), + Command.withHandler(() => TodosClient.list) +) + +const remove = Command.make("remove", { id: todoId }).pipe( + Command.withDescription("Remove a todo"), + Command.withHandler(({ id }) => TodosClient.remove(id)) +) + +const command = Command.make("todo").pipe( + Command.withSubcommands([add, done, list, remove]) +) + +export const cli = Command.run(command, { + name: "Todo CLI", + version: "0.0.0" +}) diff --git a/templates/monorepo/packages/cli/src/TodosClient.ts b/templates/monorepo/packages/cli/src/TodosClient.ts new file mode 100644 index 0000000..e020494 --- /dev/null +++ b/templates/monorepo/packages/cli/src/TodosClient.ts @@ -0,0 +1,47 @@ +import { HttpApiClient } from "@effect/platform" +import { TodosApi } from "@template/domain/TodosApi" +import { Effect, Layer } from "effect" + +export const make = Effect.gen(function*() { + const client = yield* HttpApiClient.make(TodosApi, { + baseUrl: "http://localhost:3000" + }) + + function create(text: string) { + return client.todos.createTodo({ payload: { text } }).pipe( + Effect.flatMap((todo) => Effect.logInfo("Created todo: ", todo)) + ) + } + + const list = client.todos.getAllTodos().pipe( + Effect.flatMap((todos) => Effect.logInfo(todos)) + ) + + function complete(id: number) { + return client.todos.completeTodo({ path: { id } }).pipe( + Effect.flatMap((todo) => Effect.logInfo("Marked todo completed: ", todo)), + Effect.catchTag("TodoNotFound", () => Effect.logError(`Failed to find todo with id: ${id}`)) + ) + } + + function remove(id: number) { + return client.todos.removeTodo({ path: { id } }).pipe( + Effect.flatMap(() => Effect.logInfo(`Deleted todo with id: ${id}`)), + Effect.catchTag("TodoNotFound", () => Effect.logError(`Failed to find todo with id: ${id}`)) + ) + } + + return { + create, + list, + complete, + remove + } as const +}) + +export class TodosClient extends Effect.Tag("cli/TodosClient")< + TodosClient, + Effect.Effect.Success +>() { + static Live = Layer.effect(this, make) +} diff --git a/templates/monorepo/packages/cli/src/bin.ts b/templates/monorepo/packages/cli/src/bin.ts new file mode 100644 index 0000000..f07dfb0 --- /dev/null +++ b/templates/monorepo/packages/cli/src/bin.ts @@ -0,0 +1,16 @@ +#!/usr/bin/env node + +import { NodeContext, NodeHttpClient, NodeRuntime } from "@effect/platform-node" +import { Effect, Layer } from "effect" +import { cli } from "./Cli.js" +import { TodosClient } from "./TodosClient.js" + +const MainLive = TodosClient.Live.pipe( + Layer.provide(NodeHttpClient.layerUndici), + Layer.merge(NodeContext.layer) +) + +cli(process.argv).pipe( + Effect.provide(MainLive), + NodeRuntime.runMain +) diff --git a/templates/monorepo/packages/cli/tsconfig.build.json b/templates/monorepo/packages/cli/tsconfig.build.json index 152f93d..7141a0f 100644 --- a/templates/monorepo/packages/cli/tsconfig.build.json +++ b/templates/monorepo/packages/cli/tsconfig.build.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.src.json", + "references": [ + { "path": "../domain/tsconfig.build.json" } + ], "compilerOptions": { "types": ["node"], "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", diff --git a/templates/monorepo/packages/cli/tsconfig.src.json b/templates/monorepo/packages/cli/tsconfig.src.json index 375b1f0..554459b 100644 --- a/templates/monorepo/packages/cli/tsconfig.src.json +++ b/templates/monorepo/packages/cli/tsconfig.src.json @@ -1,6 +1,9 @@ { "extends": "../../tsconfig.base.json", "include": ["src"], + "references": [ + { "path": "../domain" } + ], "compilerOptions": { "types": ["node"], "outDir": "build/src", diff --git a/templates/monorepo/packages/cli/tsconfig.test.json b/templates/monorepo/packages/cli/tsconfig.test.json index 2fbcc00..1b52ed0 100644 --- a/templates/monorepo/packages/cli/tsconfig.test.json +++ b/templates/monorepo/packages/cli/tsconfig.test.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "include": ["test"], "references": [ - { "path": "tsconfig.src.json" } + { "path": "tsconfig.src.json" }, + { "path": "../domain" } ], "compilerOptions": { "types": ["node"], diff --git a/templates/monorepo/packages/domain/package.json b/templates/monorepo/packages/domain/package.json index 3576188..550ac5e 100644 --- a/templates/monorepo/packages/domain/package.json +++ b/templates/monorepo/packages/domain/package.json @@ -13,6 +13,7 @@ "coverage": "vitest --coverage" }, "dependencies": { + "@effect/platform": "^0.64.0", "@effect/schema": "latest", "@effect/sql": "latest", "effect": "latest" diff --git a/templates/monorepo/packages/domain/src/Todo.ts b/templates/monorepo/packages/domain/src/Todo.ts deleted file mode 100644 index fb887e1..0000000 --- a/templates/monorepo/packages/domain/src/Todo.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Schema } from "@effect/schema" -import { Model } from "@effect/sql" - -export const TodoId = Schema.Number.pipe(Schema.brand("TodoId")) -export type TodoId = typeof TodoId.Type - -export const TodoIdFromString = Schema.NumberFromString.pipe( - Schema.compose(TodoId) -) - -export class Todo extends Model.Class("Todo")({ - id: Model.Generated(TodoId), - task: Schema.NonEmptyTrimmedString, - createdAt: Model.DateTimeInsert, - updatedAt: Model.DateTimeUpdate -}) {} - -export class TodoNotFound extends Schema.TaggedError()("TodoNotFound", { - id: TodoId -}) {} diff --git a/templates/monorepo/packages/domain/src/TodosApi.ts b/templates/monorepo/packages/domain/src/TodosApi.ts new file mode 100644 index 0000000..41d28af --- /dev/null +++ b/templates/monorepo/packages/domain/src/TodosApi.ts @@ -0,0 +1,56 @@ +import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform" +import { Schema } from "@effect/schema" + +export const TodoId = Schema.Number.pipe(Schema.brand("TodoId")) +export type TodoId = typeof TodoId.Type + +export const TodoIdFromString = Schema.NumberFromString.pipe( + Schema.compose(TodoId) +) + +export class Todo extends Schema.Class("Todo")({ + id: TodoId, + text: Schema.NonEmptyTrimmedString, + done: Schema.Boolean +}) {} + +export class TodoNotFound extends Schema.TaggedError()("TodoNotFound", { + id: Schema.Number +}) {} + +export class TodosApiGroup extends HttpApiGroup.make("todos").pipe( + HttpApiGroup.add( + HttpApiEndpoint.get("getAllTodos", "/todos").pipe( + HttpApiEndpoint.setSuccess(Schema.Array(Todo)) + ) + ), + HttpApiGroup.add( + HttpApiEndpoint.get("getTodoById", "/todos/:id").pipe( + HttpApiEndpoint.setSuccess(Todo), + HttpApiEndpoint.addError(TodoNotFound, { status: 404 }), + HttpApiEndpoint.setPath(Schema.Struct({ id: Schema.NumberFromString })) + ) + ), + HttpApiGroup.add( + HttpApiEndpoint.post("createTodo", "/todos").pipe( + HttpApiEndpoint.setSuccess(Todo), + HttpApiEndpoint.setPayload(Schema.Struct({ text: Schema.NonEmptyTrimmedString })) + ) + ), + HttpApiGroup.add( + HttpApiEndpoint.patch("completeTodo", "/todos/:id").pipe( + HttpApiEndpoint.setSuccess(Todo), + HttpApiEndpoint.addError(TodoNotFound, { status: 404 }), + HttpApiEndpoint.setPath(Schema.Struct({ id: Schema.NumberFromString })) + ) + ), + HttpApiGroup.add( + HttpApiEndpoint.del("removeTodo", "/todos/:id").pipe( + HttpApiEndpoint.setSuccess(Schema.Void), + HttpApiEndpoint.addError(TodoNotFound, { status: 404 }), + HttpApiEndpoint.setPath(Schema.Struct({ id: Schema.NumberFromString })) + ) + ) +) {} + +export class TodosApi extends HttpApi.empty.pipe(HttpApi.addGroup(TodosApiGroup)) {} diff --git a/templates/monorepo/packages/server/package.json b/templates/monorepo/packages/server/package.json index 0193f4d..8944815 100644 --- a/templates/monorepo/packages/server/package.json +++ b/templates/monorepo/packages/server/package.json @@ -12,6 +12,12 @@ "test": "vitest", "coverage": "vitest --coverage" }, + "dependencies": { + "@effect/platform": "latest", + "@effect/platform-node": "latest", + "@template/domain": "workspace:^", + "effect": "latest" + }, "effect": { "generateExports": { "include": [ diff --git a/templates/monorepo/packages/server/src/Api.ts b/templates/monorepo/packages/server/src/Api.ts new file mode 100644 index 0000000..0b66511 --- /dev/null +++ b/templates/monorepo/packages/server/src/Api.ts @@ -0,0 +1,20 @@ +import { HttpApiBuilder } from "@effect/platform" +import { TodosApi } from "@template/domain/TodosApi" +import { Effect, Layer } from "effect" +import { TodosRepository } from "./TodosRepository.js" + +const TodosApiLive = HttpApiBuilder.group(TodosApi, "todos", (handlers) => + Effect.gen(function*() { + const todos = yield* TodosRepository + return handlers.pipe( + HttpApiBuilder.handle("getAllTodos", () => todos.getAll), + HttpApiBuilder.handle("getTodoById", ({ path: { id } }) => todos.getById(id)), + HttpApiBuilder.handle("createTodo", ({ payload: { text } }) => todos.create(text)), + HttpApiBuilder.handle("completeTodo", ({ path: { id } }) => todos.complete(id)), + HttpApiBuilder.handle("removeTodo", ({ path: { id } }) => todos.remove(id)) + ) + })) + +export const ApiLive = HttpApiBuilder.api(TodosApi).pipe( + Layer.provide(TodosApiLive) +) diff --git a/templates/monorepo/packages/server/src/TodosRepository.ts b/templates/monorepo/packages/server/src/TodosRepository.ts new file mode 100644 index 0000000..63c77aa --- /dev/null +++ b/templates/monorepo/packages/server/src/TodosRepository.ts @@ -0,0 +1,53 @@ +import { Todo, TodoId, TodoNotFound } from "@template/domain/TodosApi" +import { Context, Effect, HashMap, Layer, Ref } from "effect" + +const make = Effect.gen(function*() { + const todos = yield* Ref.make(HashMap.empty()) + + const getAll = Ref.get(todos).pipe( + Effect.map((todos) => Array.from(HashMap.values(todos))) + ) + + function getById(id: number): Effect.Effect { + return Ref.get(todos).pipe( + Effect.flatMap(HashMap.get(id)), + Effect.catchTag("NoSuchElementException", () => new TodoNotFound({ id })) + ) + } + + function create(text: string): Effect.Effect { + return Ref.modify(todos, (map) => { + const id = TodoId.make(HashMap.reduce(map, 0, (max, todo) => todo.id > max ? todo.id : max)) + const todo = new Todo({ id, text, done: false }) + return [todo, HashMap.set(map, id, todo)] + }) + } + + function complete(id: number): Effect.Effect { + return getById(id).pipe( + Effect.map((todo) => new Todo({ ...todo, done: true })), + Effect.tap((todo) => Ref.update(todos, HashMap.set(todo.id, todo))) + ) + } + + function remove(id: number): Effect.Effect { + return getById(id).pipe( + Effect.flatMap((todo) => Ref.update(todos, HashMap.remove(todo.id))) + ) + } + + return { + getAll, + getById, + create, + complete, + remove + } as const +}) + +export class TodosRepository extends Context.Tag("api/TodosRepository")< + TodosRepository, + Effect.Effect.Success +>() { + static Live = Layer.effect(this, make) +} diff --git a/templates/monorepo/packages/server/src/server.ts b/templates/monorepo/packages/server/src/server.ts new file mode 100644 index 0000000..f1e890d --- /dev/null +++ b/templates/monorepo/packages/server/src/server.ts @@ -0,0 +1,16 @@ +import { HttpApiBuilder, HttpMiddleware } from "@effect/platform" +import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" +import { Layer } from "effect" +import { createServer } from "node:http" +import { ApiLive } from "./Api.js" +import { TodosRepository } from "./TodosRepository.js" + +const HttpLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( + Layer.provide(ApiLive), + Layer.provide(TodosRepository.Live), + Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) +) + +Layer.launch(HttpLive).pipe( + NodeRuntime.runMain +) diff --git a/templates/monorepo/packages/server/tsconfig.build.json b/templates/monorepo/packages/server/tsconfig.build.json index 152f93d..7141a0f 100644 --- a/templates/monorepo/packages/server/tsconfig.build.json +++ b/templates/monorepo/packages/server/tsconfig.build.json @@ -1,5 +1,8 @@ { "extends": "./tsconfig.src.json", + "references": [ + { "path": "../domain/tsconfig.build.json" } + ], "compilerOptions": { "types": ["node"], "tsBuildInfoFile": ".tsbuildinfo/build.tsbuildinfo", diff --git a/templates/monorepo/packages/server/tsconfig.src.json b/templates/monorepo/packages/server/tsconfig.src.json index 375b1f0..554459b 100644 --- a/templates/monorepo/packages/server/tsconfig.src.json +++ b/templates/monorepo/packages/server/tsconfig.src.json @@ -1,6 +1,9 @@ { "extends": "../../tsconfig.base.json", "include": ["src"], + "references": [ + { "path": "../domain" } + ], "compilerOptions": { "types": ["node"], "outDir": "build/src", diff --git a/templates/monorepo/packages/server/tsconfig.test.json b/templates/monorepo/packages/server/tsconfig.test.json index 2fbcc00..9c31481 100644 --- a/templates/monorepo/packages/server/tsconfig.test.json +++ b/templates/monorepo/packages/server/tsconfig.test.json @@ -2,7 +2,8 @@ "extends": "../../tsconfig.base.json", "include": ["test"], "references": [ - { "path": "tsconfig.src.json" } + { "path": "tsconfig.src.json" }, + { "path": "../domain" } ], "compilerOptions": { "types": ["node"],