From 6f54fa0c67c4a124c383ff590b10d70e74350409 Mon Sep 17 00:00:00 2001 From: behnam Date: Sat, 2 Nov 2024 14:57:23 +0300 Subject: [PATCH] Add demo logics for using fp-ts in feature layer --- .../latest-invoices-controller.ts | 4 +- .../latest-invoices/latest-invoices.tsx | 5 +- src/feature/common/failures/base-failure.ts | 8 ++- .../common/failures/dev/arguments-failure.ts | 6 +-- .../common/failures/dev/base-dev-failure.ts | 2 +- .../common/failures/dev/dependency-failure.ts | 6 +-- .../common/failures/network-failure.ts | 6 +-- src/feature/common/failures/params-failure.ts | 12 +++++ .../data/repo/customer-invoice-db-repo.ts | 36 +++++++------ .../domain/i-repo/customer-invoice-repo.ts | 3 +- .../fetch-customer-invoices-usecase.ts | 6 +-- .../core/invoice/data/repo/invoice-db-repo.ts | 52 ++++++++++++------- .../invoice/domain/i-repo/invoice-repo.ts | 3 +- .../domain/usecase/create-invoice-usecase.ts | 25 +++++---- 14 files changed, 109 insertions(+), 65 deletions(-) create mode 100644 src/feature/common/failures/params-failure.ts diff --git a/src/app/dashboard/components/server/latest-invoices/latest-invoices-controller.ts b/src/app/dashboard/components/server/latest-invoices/latest-invoices-controller.ts index 45bde22..8fbbde9 100644 --- a/src/app/dashboard/components/server/latest-invoices/latest-invoices-controller.ts +++ b/src/app/dashboard/components/server/latest-invoices/latest-invoices-controller.ts @@ -1,5 +1,5 @@ import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase"; -export default function latestInvoicesController() { - return fetchCustomerInvoicesUsecase() +export default async function latestInvoicesController() { + return await fetchCustomerInvoicesUsecase() } \ No newline at end of file diff --git a/src/app/dashboard/components/server/latest-invoices/latest-invoices.tsx b/src/app/dashboard/components/server/latest-invoices/latest-invoices.tsx index 09396cd..696af73 100644 --- a/src/app/dashboard/components/server/latest-invoices/latest-invoices.tsx +++ b/src/app/dashboard/components/server/latest-invoices/latest-invoices.tsx @@ -2,12 +2,15 @@ import CreateRandomInvoiceContainer from '@/app/dashboard/components/client/crea import latestInvoicesController from '@/app/dashboard/components/server/latest-invoices/latest-invoices-controller'; import { ArrowPathIcon } from '@heroicons/react/24/outline'; import clsx from 'clsx'; +import { isLeft } from 'fp-ts/lib/Either'; import Image from 'next/image'; export default async function LatestInvoices() { const latestInvoices = await latestInvoicesController(); - const invoices = latestInvoices.map((invoice, i) => { + if (isLeft(latestInvoices)) return
Error
+ + const invoices = latestInvoices.right.map((invoice, i) => { return (
{ /* ------------------------------- Attributes ------------------------------- */ private readonly BASE_FAILURE_MESSAGE = "failure"; @@ -15,8 +15,12 @@ export default abstract class BaseFailure { message = this.BASE_FAILURE_MESSAGE; /* -------------------------------------------------------------------------- */ - constructor(key: string) { + metadata: META_DATA | undefined; + + /* -------------------------------------------------------------------------- */ + constructor(key: string, metadata?: META_DATA) { this.message = makeFailureMessage(this.message, key); + this.metadata = metadata ?? undefined } /* -------------------------------------------------------------------------- */ } diff --git a/src/feature/common/failures/dev/arguments-failure.ts b/src/feature/common/failures/dev/arguments-failure.ts index ed4e2c9..11d01c2 100644 --- a/src/feature/common/failures/dev/arguments-failure.ts +++ b/src/feature/common/failures/dev/arguments-failure.ts @@ -3,10 +3,10 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure"; /** * Failure for needed arguments in a method but sent wrong one */ -export default class ArgumentsFailure extends BaseDevFailure { +export default class ArgumentsFailure extends BaseDevFailure { /* ------------------------------- Constructor ------------------------------ */ - constructor() { - super("arguments"); + constructor(metadata?: META_DATA) { + super("arguments", metadata); } /* -------------------------------------------------------------------------- */ } diff --git a/src/feature/common/failures/dev/base-dev-failure.ts b/src/feature/common/failures/dev/base-dev-failure.ts index aaf8142..ad404d1 100644 --- a/src/feature/common/failures/dev/base-dev-failure.ts +++ b/src/feature/common/failures/dev/base-dev-failure.ts @@ -1,3 +1,3 @@ import BaseFailure from "@/feature/common/failures/base-failure"; -export default abstract class BaseDevFailure extends BaseFailure {} +export default abstract class BaseDevFailure extends BaseFailure {} diff --git a/src/feature/common/failures/dev/dependency-failure.ts b/src/feature/common/failures/dev/dependency-failure.ts index 86b49cf..6f16f8f 100644 --- a/src/feature/common/failures/dev/dependency-failure.ts +++ b/src/feature/common/failures/dev/dependency-failure.ts @@ -3,8 +3,8 @@ import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure"; /** * This is a failure of not having specific dependency */ -export default class DependencyFailure extends BaseDevFailure { - constructor() { - super("DependencyFailure"); +export default class DependencyFailure extends BaseDevFailure { + constructor(metadata: META_DATA) { + super("DependencyFailure", metadata); } } diff --git a/src/feature/common/failures/network-failure.ts b/src/feature/common/failures/network-failure.ts index 3268fc3..192bdf5 100644 --- a/src/feature/common/failures/network-failure.ts +++ b/src/feature/common/failures/network-failure.ts @@ -3,10 +3,10 @@ import BaseFailure from "./base-failure"; /** * Failure for HTTP response when response dosn't have base structure */ -export default class NetworkFailure extends BaseFailure { +export default class NetworkFailure extends BaseFailure { /* ------------------------------- Constructor ------------------------------ */ - constructor() { - super("network"); + constructor(metaData?: META_DATA) { + super("network", metaData); } /* -------------------------------------------------------------------------- */ } diff --git a/src/feature/common/failures/params-failure.ts b/src/feature/common/failures/params-failure.ts new file mode 100644 index 0000000..df50d8c --- /dev/null +++ b/src/feature/common/failures/params-failure.ts @@ -0,0 +1,12 @@ +import BaseFailure from "./base-failure"; + +/** + * Failure for params failure + */ +export default class ParamsFailure extends BaseFailure { + /* ------------------------------- Constructor ------------------------------ */ + constructor(metadata?: META_DATA) { + super("params", metadata); + } + /* -------------------------------------------------------------------------- */ +} diff --git a/src/feature/core/customer-invoice/data/repo/customer-invoice-db-repo.ts b/src/feature/core/customer-invoice/data/repo/customer-invoice-db-repo.ts index a12de93..bc35087 100644 --- a/src/feature/core/customer-invoice/data/repo/customer-invoice-db-repo.ts +++ b/src/feature/core/customer-invoice/data/repo/customer-invoice-db-repo.ts @@ -1,7 +1,12 @@ import { sql } from "@/bootstrap/boundaries/db/db"; +import ApiTask from "@/feature/common/data/api-task"; +import { failureOr } from "@/feature/common/failures/failure-helpers"; +import NetworkFailure from "@/feature/common/failures/network-failure"; import { formatCurrency } from "@/feature/common/feature-helpers"; import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"; import CustomerInvoiceRepo from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo"; +import { pipe } from "fp-ts/lib/function"; +import { tryCatch } from "fp-ts/lib/TaskEither"; import postgres from "postgres"; type customerInvoiceDbResponse = { @@ -13,23 +18,24 @@ type customerInvoiceDbResponse = { } export default class CustomerInvoiceDbRepo implements CustomerInvoiceRepo { - async fetchList(): Promise { - try { - const data = await sql` - SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id - FROM invoices - JOIN customers ON invoices.customer_id = customers.id - ORDER BY invoices.date DESC - LIMIT 20 ` as postgres.RowList; - - return this.customerInvoicesDto(data) - } catch (error) { - console.error('Database Error:', error); - throw new Error('Failed to fetch the latest invoices.'); - } + fetchList(): ApiTask { + return pipe( + tryCatch( + async () => { + const response = await sql` + SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id + FROM invoices + JOIN customers ON invoices.customer_id = customers.id + ORDER BY invoices.date DESC + LIMIT 20 ` as postgres.RowList; + + return this.customerInvoicesDto(response) + }, + (l) => failureOr(l, new NetworkFailure()) + ) + ) } - private customerInvoicesDto(dbCustomers: customerInvoiceDbResponse[]): CustomerInvoice[] { return dbCustomers.map((customer) => this.customerInvoiceDto(customer)); } diff --git a/src/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo.ts b/src/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo.ts index 0a8c993..ab4b486 100644 --- a/src/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo.ts +++ b/src/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo.ts @@ -1,7 +1,8 @@ +import ApiTask from "@/feature/common/data/api-task" import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice" export default interface CustomerInvoiceRepo { - fetchList(): Promise + fetchList(): ApiTask } export const customerInvoiceRepoKey = "customerInvoiceRepoKey" \ No newline at end of file diff --git a/src/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts b/src/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts index 5947553..5d2df55 100644 --- a/src/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts +++ b/src/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase.ts @@ -1,12 +1,12 @@ -"use server" +import { ApiEither } from "@/feature/common/data/api-task"; import serverDi from "@/feature/common/server-di"; import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice"; import CustomerInvoiceRepo, { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo"; import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key"; import { connection } from "next/server"; -export default async function fetchCustomerInvoicesUsecase(): Promise { +export default async function fetchCustomerInvoicesUsecase(): Promise> { connection() const repo = serverDi(customerInvoiceModuleKey).resolve(customerInvoiceRepoKey) - return repo.fetchList() + return repo.fetchList()() } \ No newline at end of file diff --git a/src/feature/core/invoice/data/repo/invoice-db-repo.ts b/src/feature/core/invoice/data/repo/invoice-db-repo.ts index c8e2bb9..416c060 100644 --- a/src/feature/core/invoice/data/repo/invoice-db-repo.ts +++ b/src/feature/core/invoice/data/repo/invoice-db-repo.ts @@ -1,8 +1,13 @@ import { sql } from "@/bootstrap/boundaries/db/db"; +import ApiTask from "@/feature/common/data/api-task"; +import { failureOr } from "@/feature/common/failures/failure-helpers"; +import NetworkFailure from "@/feature/common/failures/network-failure"; import { formatCurrency } from "@/feature/common/feature-helpers"; import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo"; import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param"; import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status"; +import { pipe } from "fp-ts/lib/function"; +import { tryCatch } from "fp-ts/lib/TaskEither"; import postgres from "postgres"; type InvoiceSummaryDbResponse = {paid: string, pending: string} @@ -13,26 +18,33 @@ export default class InvoiceDbRepo implements InvoiceRepo { return data.count ?? 0 } - async createInvoice(params: InvoiceParam): Promise { - const firstCustomerIdDb = await sql`SELECT - id FROM customers - ORDER BY id DESC - LIMIT 1 - ` - const customerId = firstCustomerIdDb.at(0)?.id - if (!customerId) throw new Error("There is no customer") - - const { amount, status } = params; - const amountInCents = amount * 100; - const date = new Date().toISOString().split('T')[0]; - - // Insert data into the database - const result = await sql` - INSERT INTO invoices (customer_id, amount, status, date) - VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) - RETURNING id - `; - return result.at(0)?.id ?? "" + createInvoice(params: InvoiceParam): ApiTask { + return pipe( + tryCatch( + async () => { + const firstCustomerIdDb = await sql`SELECT + id FROM customers + ORDER BY id DESC + LIMIT 1 + ` + const customerId = firstCustomerIdDb.at(0)?.id + if (!customerId) throw new Error("There is no customer") + + const { amount, status } = params; + const amountInCents = amount * 100; + const date = new Date().toISOString().split('T')[0]; + + // Insert data into the database + const result = await sql` + INSERT INTO invoices (customer_id, amount, status, date) + VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) + RETURNING id + `; + return result.at(0)?.id ?? "" + }, + (l) => failureOr(l, new NetworkFailure(l as Error)) + ), + ) } async fetchInvoicesStatusSummary(): Promise { diff --git a/src/feature/core/invoice/domain/i-repo/invoice-repo.ts b/src/feature/core/invoice/domain/i-repo/invoice-repo.ts index 983ac3f..ca5096d 100644 --- a/src/feature/core/invoice/domain/i-repo/invoice-repo.ts +++ b/src/feature/core/invoice/domain/i-repo/invoice-repo.ts @@ -1,10 +1,11 @@ +import ApiTask from "@/feature/common/data/api-task" import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param" import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status" export default interface InvoiceRepo { fetchAllInvoicesAmount(): Promise fetchInvoicesStatusSummary(): Promise - createInvoice(params: InvoiceParam): Promise + createInvoice(params: InvoiceParam): ApiTask } export const invoiceRepoKey = "invoiceRepoKey" \ No newline at end of file diff --git a/src/feature/core/invoice/domain/usecase/create-invoice-usecase.ts b/src/feature/core/invoice/domain/usecase/create-invoice-usecase.ts index 73a0106..9cfc63d 100644 --- a/src/feature/core/invoice/domain/usecase/create-invoice-usecase.ts +++ b/src/feature/core/invoice/domain/usecase/create-invoice-usecase.ts @@ -1,19 +1,24 @@ "use server" +import { ApiEither } from "@/feature/common/data/api-task"; +import ParamsFailure from "@/feature/common/failures/params-failure"; import serverDi from "@/feature/common/server-di"; import InvoiceRepo, { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo"; import { InvoiceParam, invoiceSchema } from "@/feature/core/invoice/domain/param/invoice-param"; import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key"; +import { pipe } from "fp-ts/lib/function"; +import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither"; -export default async function createInvoiceUsecase(params: InvoiceParam): Promise { - const isParamsValid = invoiceSchema.safeParse(params) - - if (!isParamsValid) { - return { - errorMessage: "Please pass correct params" - } - } +export default async function createInvoiceUsecase(params: InvoiceParam): Promise> { const repo = serverDi(invoiceModuleKey).resolve(invoiceRepoKey) - return repo.createInvoice(params) - + return pipe( + fromNullable(new ParamsFailure())(params), + map((params) => invoiceSchema.safeParse(params)), + chain((params) => { + const isParamsValid = invoiceSchema.safeParse(params) + if (!isParamsValid.success) left(new ParamsFailure()) + return right(params.data as InvoiceParam) + }), + chain((params) => repo.createInvoice(params)) + )() } \ No newline at end of file