Skip to content

Commit

Permalink
feat: add lists & policy into domain model and refactor modules and a…
Browse files Browse the repository at this point in the history
…pps to conform to new spec. Also decouple domain models from interactor request/response interfaces
  • Loading branch information
alexx666 committed Apr 25, 2021
1 parent 97a9f3c commit 0655056
Show file tree
Hide file tree
Showing 36 changed files with 460 additions and 178 deletions.
1 change: 1 addition & 0 deletions docs/clean-arch-todos.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mxfile host="Electron" modified="2021-04-25T09:23:17.868Z" agent="5.0 (Macintosh; Intel Mac OS X 11_2_3) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/14.5.1 Chrome/89.0.4389.82 Electron/12.0.1 Safari/537.36" etag="jGKru9uW8Jtk9Hhmri8f" version="14.5.1" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-1">7Vttc9o4EP41zLQ3Q8cvQMjHAHm5HtejJWnaj8IWRo1scbYIkF/flS1jG9lgqCENZYaZWGt5Le3z7Eq7Vmpm113c+mg6+ZfZmNYMzV7UzF7NMHS93YI/QrKMJC1NjwSOT2zZKREMyQuWQk1KZ8TGQaYjZ4xyMs0KLeZ52OIZGfJ9Ns92GzOafesUOVgRDC1EVekjsfkkkraNi0R+h4kzid+sty6jOy6KO8uZBBNks3lKZF7XzK7PGI+u3EUXU2G82C6Pfy8faf+pdfvxc/A/euj8c//paz1SdrPLI6sp+Njje6t+eRrf3H1t/Pg2Hdx8nmu3V6OPdfmI9ozoTNqrTwIu58uXsRGDOXEp8qDVGTOPD+UdMEEHUeJ4cG3B2LAPgmfscwL2v5I3OJuC1JoQavfRks3EDAKOrKe41Zkwn7yAWkThlg4CuO1zSSWjlekxFE+CWAOpjwPoM4jNoq9EfRRw2cdilKJpQEbhgEUXF/kO8TqMc+bGitjMs7EtWyucwwb32dOKOeL5kmBI0IQ18CJFRQnOLWYu5v4SuqxcTYIhHU2PeTdPaKu3pGySoixIpbtIV3FWulev+wKuhTwHjLB6n2Guva9R9n1rr0MUgPcQxx1hxiBNQrhITTURhdTMp+mg9dBCD8Ph3fdH2v1vrNmfzGX9UqGph1y4ugpp6hPPUSgLVucpelI85oXkDKbIAh39sE+vkUi+yFkLEYNnxzQkxoTYNvZC4nDEUcQtwZYpIx4PzdLswA+M19U+NGtNGFAX2nrShp/o7vMu82AGiIRkwkDcORbk3cTJCXepvCwgWZafxZ6vknGZxXhXLqSplyHBrojramQKwoAgIPdm7ghizRnyKiFvGseDPHd4hoL4lFFiLSPMxbo0iNp/Mu7lQC617kjcL9qvjHtDwV1BmJJw7yGtoeeuy1vgdwFIoS7G+17QoVfXFU6YKifMHPwpGmE6YAHhhAn9ftR3jRfboD8UqLrWKIdq+0CgNhVQkW2/u2c2e39y/nswEPUjhuTcVbitoOhjF6x70kBWv7Tq5hFjbC6QFwqQDg4zrDOSuyHZKhlXD7Za6iqUAkIFwYKUXTun7NWl7E1N+vWuKbt+qRXTozhlbzZ/y5Q9n6fq2iH6n3DGXjLArBy49D7gmMl4/pDV+ouNA8sn02jze8KgVpqb7Q796yflOXUYEep7EDwi4MOrM+yVwv7qOXm8XUrBjj37DPohQS+dsx+u6KqWYqj4HHQO8PvnExEPNqSGe27hqnN1UwFdAflPr7/t4czNkpu2QxXgDNWXSTCAmfdm+N37yKVHjFGMvJPz6cOh+urpf6x4k7fCSn0lTk5Ai01DW2/Jj7MxEB6/ITR2dGw7OC4eYDpi8+tE0AkFcCOmxs65dsBmvoU3zFfaFracDi6DpRjuRiQlzoJK6VS6IQfkY4o4ec4eHtmQpQ8EaxO1ZnstQ780siqiCcunEi4oipqNLYoii2xXVF9TZK7pYeNxgDMqqqoAGGqlKkY94WpY/UlFgS0xZyRrQFH47yDryQnJ22WU+XDfY0nFKypG6Ub5gBA7lzyLJIdVW50AKl8Zqv8inWJWHg0rtVrzV2ms4qX3rYJVjevHNfYDQpe7aW+p8T8VsmGVnDCHeYim43Y24Cd9+izclYm4/wNzvpSwoBln2e0fXhD+TTwOYTRqfY9XD7juLdKNZWYPvRag91sq8rOXkktFQYW/8si/VuItCNhV0SD+VrjRg4Xx+8IZs2hu/SCw2ryvhwB1t13gfMV+msa/mOAbFvKLi7g+KU0fnyTZF9HD+6yu5lzq0niCYNW1D22j0cqi9Yvh9whoKWBtOIx0Piq7/t1tS9WkOOaXPirbKFtEaWtbafXGT8qqgcVFC/GBOFCY+taz/Aord8Vu/1t8lMsd3vnYXPWglv7c1tgZVGgm/yoSeXvyDzfm9U8=</diagram></mxfile>
93 changes: 93 additions & 0 deletions docs/clean-arch-todos.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"info": {
"_postman_id": "fbfcbd94-f3ab-4fb0-bcb4-d0134177a3d7",
"name": "clean-arch-todos",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "ListTodos",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "{{url}}/lists/{{listName}}/todos?limit={{limit}}",
"host": [
"{{url}}"
],
"path": [
"lists",
"{{listName}}",
"todos"
],
"query": [
{
"key": "limit",
"value": "{{limit}}"
},
{
"key": "marker",
"value": "3060961b-6afc-466b-9032-715ea52dc199",
"disabled": true
}
]
}
},
"response": []
},
{
"name": "CreateTodo",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"description\": \"{{description}}\",\n \"start\": \"{{due}}\",\n \"end\": \"{{due}}\"\n}",
"options": {
"raw": {
"language": "json"
}
}
},
"url": {
"raw": "{{url}}/lists/{{listName}}/todos",
"host": [
"{{url}}"
],
"path": [
"lists",
"{{listName}}",
"todos"
]
}
},
"response": []
},
{
"name": "DeleteTodo",
"request": {
"method": "DELETE",
"header": [],
"url": {
"raw": "{{url}}/lists/{{listName}}/todos/:id",
"host": [
"{{url}}"
],
"path": [
"lists",
"{{listName}}",
"todos",
":id"
],
"variable": [
{
"key": "id",
"value": "3060961b-6afc-466b-9032-715ea52dc199"
}
]
}
},
"response": []
}
]
}
8 changes: 4 additions & 4 deletions jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 90,
"functions": 90,
"lines": 90,
"statements": -10
"branches": 80,
"functions": 80,
"lines": 80,
"statements": -20
}
}
}
2 changes: 1 addition & 1 deletion src/api/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const API = Express();

API.use(morgan("combined"));

API.use("/todos", todoRouter)
API.use("/lists/:name/todos", todoRouter)

API.use("*", (err: Error, _1: any, res: Response, _2: any) => res.status(500).json({ error: err.message }))

Expand Down
4 changes: 2 additions & 2 deletions src/api/routers/todos/create.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { json } from "body-parser";
import { CreateTodo, CreateTodoRequest } from "../../../modules/todos/boundry/create-todo";

export default function(createTodo: CreateTodo) {
const createRouter = Router()
const createRouter = Router({ mergeParams: true })

createRouter.post("/", json(), async (req: Request, res: Response, next: NextFunction) => {
try {
const request = req.body as CreateTodoRequest;
const request: CreateTodoRequest = { ...req.body, listName: decodeURI(req.params.name) };
const response = await createTodo.execute(request);

res.status(200).json(response);
Expand Down
4 changes: 2 additions & 2 deletions src/api/routers/todos/delete.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Router, NextFunction, Request, Response } from "express";
import { DeleteTodo, DeleteTodoRequest } from "../../../modules/todos/boundry/delete-todo";

export default function(deleteTodo: DeleteTodo) {
const deleteRouter = Router()
const deleteRouter = Router({ mergeParams: true })

deleteRouter.delete("/:id", async (req: Request, res: Response, next: NextFunction) => {
try {
const request: DeleteTodoRequest = { id: req.params.id };
const request: DeleteTodoRequest = { listName: req.params.name, id: req.params.id };
const response = await deleteTodo.execute(request);

res.status(200).json(response);
Expand Down
2 changes: 1 addition & 1 deletion src/api/routers/todos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const createTodo = new CreateTodoImpl(repository, uuidGenerator);
export const deleteTodo = new DeleteTodoImpl(repository);
export const listTodos = new ListTodosImpl(repository);

const todoRouter = Router()
const todoRouter = Router({ mergeParams: true })

todoRouter.use(listRouter(listTodos))
todoRouter.use(createRouter(createTodo))
Expand Down
23 changes: 11 additions & 12 deletions src/api/routers/todos/list.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,38 @@ import { ListTodos, ListTodosRequest } from "../../../modules/todos/boundry/list
import Link from "../hateoas";

export default function(listTodos: ListTodos) {
const listRouter = Router()
const listRouter = Router({ mergeParams: true })

listRouter.get("/", async (req: Request, res: Response, next: NextFunction) => {
try {

const { baseUrl, query, protocol, hostname, originalUrl } = req;
const { baseUrl, query, protocol, hostname, originalUrl, params } = req;

const request: ListTodosRequest = {
listName: String(params.name),
limit: Number(query.limit) || 20,
marker: String(query.marker)
}

const { items, count } = await listTodos.execute(request)
const response = await listTodos.execute(request)

const { items, count } = response;

const links: Link[] = [
{
rel: "self",
href: `${protocol}://${hostname}${originalUrl}`
}
{ rel: "self", href: `${protocol}://${hostname}${originalUrl}` }
];

if (request.limit <= count) {
const nextPage: any = { limit: query.limit };

if (count > 0) nextPage.marker = items[count - 1].id

links.push({
rel: "next",
href: `${protocol}://${hostname}${baseUrl}?${Object.entries(nextPage).map(e => e.join('=')).join('&')}`
})
const nextQueryParams = Object.entries(nextPage).map(e => e.join('=')).join('&')

links.push({ rel: "next", href: `${protocol}://${hostname}${baseUrl}?${encodeURI(nextQueryParams)}`})
}

res.status(200).json({ items, count, links });
res.status(200).json({ ...response, links });
} catch (error) {
next(error)
}
Expand Down
11 changes: 9 additions & 2 deletions src/cli/commands/todos/create-todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,18 @@ export default function (createTodo: CreateTodo) {
return new Command("create")
.alias("mk")
.description("Create todo")
.requiredOption("-l, --list-name <listName>", "todo list name")
.requiredOption("-d, --description <description>", "todo description")
.requiredOption("-t, --due <due>", "todo due date in ISO format")
.requiredOption("-s, --start <start>", "todo start date in ISO format")
.requiredOption("-e, --end <end>", "todo end date in ISO format")
.action(async cmd => {
try {
const request: CreateTodoRequest = { description: cmd.description, due: cmd.due };
const request: CreateTodoRequest = {
listName: cmd.listName,
description: cmd.description,
start: cmd.start,
end: cmd.end
};

const result = await createTodo.execute(request);

Expand Down
3 changes: 2 additions & 1 deletion src/cli/commands/todos/delete-todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ export default function (deleteTodo: DeleteTodo) {
return new Command("delete")
.alias("rm")
.description("Delete todo")
.requiredOption("-l, --list-name <list>", "List name")
.requiredOption("--id <id>", "todo ID")
.action(async cmd => {
try {
const request: DeleteTodoRequest = { id: cmd.id };
const request: DeleteTodoRequest = { listName: cmd.listName, id: cmd.id };

const { item } = await deleteTodo.execute(request);

Expand Down
5 changes: 4 additions & 1 deletion src/cli/commands/todos/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import listCmd from "./list-todos";
import createCmd from "./create-todo";
import deleteCmd from "./delete-todo";

const todoGateway = new RestTodoGateway()
// config
import config from "../../config";

const todoGateway = new RestTodoGateway(config)
const uuidGenerator = new V4UuidGenerator()

const listTodos = new ListTodosImpl(todoGateway)
Expand Down
2 changes: 2 additions & 0 deletions src/cli/commands/todos/list-todos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ export default function (listTodos: ListTodos) {
return new Command("list")
.alias("ls")
.description("List todos")
.requiredOption("-l, --list-name <list>", "name of the list to fetch")
.option("--limit <limit>", "number of items to fetch", "20")
.option("--marker <marker>", "cursor next show all items after this one", "0")
.action(async cmd => {
try {
const request: ListTodosRequest = {
listName: String(cmd.listName),
limit: Number(cmd.limit),
marker: String(cmd.marker)
}
Expand Down
4 changes: 4 additions & 0 deletions src/cli/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
host: String(process.env.HOST),
port: Number(process.env.PORT)
}
5 changes: 4 additions & 1 deletion src/modules/shared/entity.gateway.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
export interface ReadableGateway<T> {
find(query: any): Promise<T[]>;
count(query: any): Promise<number>;
}

export interface WritableGateway<T> {
save(item: T): Promise<T>;
delete(id: string): Promise<T>;
delete(list: string, id: string): Promise<T>;
}

export interface Gateway<T> extends ReadableGateway<T>, WritableGateway<T> {}
6 changes: 4 additions & 2 deletions src/modules/todos/boundry/create-todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ export interface CreateTodo {

export interface CreateTodoRequest {
description: string;
due: string;
start: string;
end: string;
listName: string;
}

export interface CreateTodoResponse extends CreateTodoRequest {
export interface CreateTodoResponse {
id: string;
}
10 changes: 9 additions & 1 deletion src/modules/todos/boundry/delete-todo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@ export interface DeleteTodo {
}

export interface DeleteTodoRequest {
listName: string;
id: string;
}

export interface DeleteTodoResponse {
item: any
item: TodoItem
}

export interface TodoItem {
id: string;
start: string;
end: string;
description: string;
}
16 changes: 13 additions & 3 deletions src/modules/todos/boundry/list-todos.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
export interface ListTodosRequest {
limit: number
marker?: string
listName: string;
limit: number;
marker?: string;
}

export interface ListTodosResponse {
items: any[];
items: TodoItem[];
listName: string;
count: number;
}

export interface TodoItem {
id: string;
start: string;
end: string;
expired: boolean;
description: string;
}

export interface ListTodos {
/**
* Lists all todos that satisfy the query
Expand Down
3 changes: 3 additions & 0 deletions src/modules/todos/entities/list-policy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
describe("[ListPolicy] Test Cases", () => {
xit("should create a list policy successfully", () => {})
})
9 changes: 9 additions & 0 deletions src/modules/todos/entities/list-policy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import List from "./list";

export default class ListPolicy {
constructor(private maxTodos: number = 10) {}

public isAllowedToAdd(list: List): boolean {
return this.maxTodos > list.getSize();
}
}
3 changes: 3 additions & 0 deletions src/modules/todos/entities/list.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
describe("[List] Test Cases", () => {
xit("should create a list successfully", () => {})
})
28 changes: 28 additions & 0 deletions src/modules/todos/entities/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import ListPolicy from "./list-policy";
import Todo from "./todo";

export default class List {

constructor(
public readonly name: string,
private size: number = 0,
private policy: ListPolicy = new ListPolicy(10),
) {}

add(todo: Todo) {
if (!this.policy.isAllowedToAdd(this)) throw new Error("ListError: List has maximum number of allowed todos");
if (todo.isExpired) throw new Error("ListError: Can't add an expired todo to list");

// TODO: find a way to persist the todos w/o detailing method
this.size += 1;
}

remove(todo: Todo) {
// TODO: implement
this.size -= 1;
}

getSize(): number {
return this.size;
}
}
Loading

0 comments on commit 0655056

Please sign in to comment.