Compare commits
21 Commits
Author | SHA1 | Date | |
---|---|---|---|
77b167c695 | |||
da78c0e5b5 | |||
e5e2fd7b59 | |||
![]() |
278a3cb67b | ||
![]() |
4291fa832a | ||
![]() |
da8bbc5c9e | ||
![]() |
86d6b52a39 | ||
![]() |
1f6431eec6 | ||
![]() |
6ac70ac455 | ||
a2febc9030 | |||
0d18a63956 | |||
![]() |
5a29c54a6a | ||
![]() |
75b1671982 | ||
![]() |
4ee7d4b8e3 | ||
![]() |
4306ea1bdc | ||
![]() |
7d9a6e77bd | ||
![]() |
38f0d0a596 | ||
![]() |
096496df64 | ||
86732339ea | |||
![]() |
3a44fdafb6 | ||
693f07528e |
@ -1,13 +1,14 @@
|
|||||||
import React, { useRef } from "react";
|
import React from "react";
|
||||||
import { themes } from '@storybook/theming';
|
import { themes } from '@storybook/theming';
|
||||||
import { ThemeProvider } from "../src/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
|
import { ThemeProvider } from "../src/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
|
||||||
import { DARK_MODE_EVENT_NAME, UPDATE_DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
|
import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode';
|
||||||
import { initI18next, LANGS } from "../src/bootstrap/i18n/i18n"
|
import { getI18n, LANGS } from "../src/bootstrap/i18n/i18n"
|
||||||
import { addons } from '@storybook/preview-api';
|
import { addons } from '@storybook/preview-api';
|
||||||
import { i18n } from "i18next";
|
import { i18n } from "i18next";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
const channel = addons.getChannel();
|
const channel = addons.getChannel();
|
||||||
import "../src/app/globals.css"
|
import "../src/app/globals.css"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* This function will expand the object with nested properties
|
* This function will expand the object with nested properties
|
||||||
@ -69,7 +70,7 @@ const preview = {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
setI18n((await initI18next({ lng: locale })).i18n);
|
setI18n((await getI18n({ lng: locale })).i18n);
|
||||||
})()
|
})()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
25
README.md
25
README.md
@ -1,4 +1,27 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# Nextjs clean architecture boilerplate
|
||||||
|
|
||||||
|
## Table of content
|
||||||
|
|
||||||
|
- Overview
|
||||||
|
- Technologies
|
||||||
|
- Architecture
|
||||||
|
- Folder Structure
|
||||||
|
- Getting started
|
||||||
|
- Guildline
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This project is a starting point for your medium to large scale projects with Nextjs, to make sure having a structured, maintainable and reusable base for your project based on best practices in clean architecture, DDD approach for business logics, MVVM for the frontend part, storybook and vitest for testing logics and ui part and also functional programming with error handling for business logics.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
Nextjs and many other new SSR tools provide a really good and new approach to handle frontend applications, with new tools to bring a new good experience for users. But as they're new and they just tried to bring new tools and features and also frontend community, didn't talk about software engineering and best practices approach for this tools.
|
||||||
|
|
||||||
|
So in many cases we see many teams uses nextjs to just use its tools and features as much as they can but they don't care about the best practices, architecture and software engineering approach. So there are many projects with Nextjs which is not maintainable and specially in medium to large scale applications, through the time it'll be really hard to manage to even impossible to add new features and it cause business failures.
|
||||||
|
|
||||||
|
So I decided to make a base firm and maintanable boilerplate for most faviorite SSR framework, which is Nextjs and use all my experiences and best practices which fits Nextjs features and abilities to make a structured, robust and maintanable basement for SSR projects.
|
||||||
|
|
||||||
|
I personally used this boilerplate for several enterprise web-applications and it's completely tested and you can rest assured to use it safely.
|
||||||
|
|
||||||
|
> Note: I'll be happy to get your issues and problems or any other opinion to make it better together. To know how to contribute please visite the CONTRIBUTE.md file.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
@startuml Failure Class Diagram
|
||||||
|
abstract BaseFailure {
|
||||||
|
- String BASE_FAILURE_MESSAGE = "failure"
|
||||||
|
+ String message = this.BASE_FAILURE_MESSAGE
|
||||||
|
|
||||||
|
constructor(key: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserFailure {
|
||||||
|
constructor(key: string)
|
||||||
|
}
|
||||||
|
|
||||||
|
note left
|
||||||
|
Extends parent message key by sending
|
||||||
|
`user` keyword as a domain to parent.
|
||||||
|
So in `UserFailure` our `message`
|
||||||
|
property will be `failure.user`.
|
||||||
|
end note
|
||||||
|
|
||||||
|
UserFailure --|> BaseFailure
|
||||||
|
|
||||||
|
|
||||||
|
class UserModificationFailure {
|
||||||
|
constructor(key: string)
|
||||||
|
}
|
||||||
|
note right
|
||||||
|
Extends parent message key by sending
|
||||||
|
`modification` keyword to parent.
|
||||||
|
So in `UserModificationFailure` our `message`
|
||||||
|
property will be `failure.user.modification`
|
||||||
|
end note
|
||||||
|
UserModificationFailure --|> UserFailure
|
||||||
|
|
||||||
|
class UserModificationAlreadyExistsFailure {
|
||||||
|
constructor()
|
||||||
|
}
|
||||||
|
note left
|
||||||
|
Extends parent message key by sending
|
||||||
|
`alreadyExists` keyword to parent.
|
||||||
|
So in `UserModificationAlreadyExistsFailure`
|
||||||
|
our `message` property will be
|
||||||
|
`failure.user.modification.AlreadyExists`
|
||||||
|
end note
|
||||||
|
|
||||||
|
class DeviceFailure {
|
||||||
|
constructor()
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceFailure --|> BaseFailure
|
||||||
|
UserModificationAlreadyExistsFailure --|> UserModificationFailure
|
||||||
|
|
||||||
|
|
||||||
|
@enduml
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
246
catalog/docs/failure-error-handling/failure-error-handling.md
Normal file
246
catalog/docs/failure-error-handling/failure-error-handling.md
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# Error handling with failure
|
||||||
|
|
||||||
|
## What is failure
|
||||||
|
In each application and in each logic, there can be failures in the process, and based on their complexity, there can be a few or many possible scenarios for these failures.
|
||||||
|
|
||||||
|
In software development, we always try to have more control over these failures to:
|
||||||
|
|
||||||
|
Avoid possible bugs
|
||||||
|
Help users understand the state of the application with proper messages
|
||||||
|
Control processes for side effects
|
||||||
|
Monitor the behavior of the application
|
||||||
|
So, having a specific way of handling errors to achieve all these requirements in our app helps us build a more robust, reliable, and maintainable application.
|
||||||
|
|
||||||
|
Many frameworks provide their own ways to handle these failures, which they may call exceptions, failures, or other terms. But we shouldn't always depend on the behavior of frameworks for our logic and apps. Besides, many frameworks do not provide error handling tools, so we need to design a reliable architecture for the error handling in our application.
|
||||||
|
|
||||||
|
## Failure handling with base failure
|
||||||
|
To have granular control over failures and define a specific type for all errors, we can use the power of inheritance and abstraction in OOP.
|
||||||
|
|
||||||
|
We can define an abstract class as our base failure, which serves as the specific type for all failures in the application.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default abstract class BaseFailure<META_DATA> {
|
||||||
|
|
||||||
|
metadata: META_DATA | undefined;
|
||||||
|
|
||||||
|
constructor(metadata?: META_DATA) {
|
||||||
|
this.metadata = metadata ?? undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
As you can see, it's just a simple abstract class that takes some metadata about the error details in any form. But wait, this is just the beginning of the story, we can explore many ideas with this failure.
|
||||||
|
|
||||||
|
## How to write a simple failure
|
||||||
|
So, to create a simple failure, we can define our failure in any domain for any scenario we need, like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default class CreateUserFailure extends BaseFailure<{ userId: string }> {
|
||||||
|
constructor(metadata?: { userId: string }) {
|
||||||
|
super(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
So in our logics for creating user we can return specific type of failure for creating user.
|
||||||
|
|
||||||
|
## Combination with Functional programming
|
||||||
|
Functional programming is a deep topic that we cannot fully cover here. For more details, you can check out various courses and books available online.
|
||||||
|
|
||||||
|
However, for this article, we focus on one of the most useful functors in functional programming and how failure fits perfectly with it. This concept is the Either type.
|
||||||
|
|
||||||
|
Either is an algebraic data type (ADT) that represents computations that can return either a success or a failure. It consists of two possible values:
|
||||||
|
|
||||||
|
- Right(value): Represents a successful result.
|
||||||
|
- Left(error): Represents a failure or unexpected result.
|
||||||
|
|
||||||
|
You're guessing right, our base failure will serve as the Left value in Either, allowing us to handle errors in a structured and functional way.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Either<
|
||||||
|
BaseFailure<unknown>,
|
||||||
|
ResponseType
|
||||||
|
>
|
||||||
|
```
|
||||||
|
So as we always have specific type for handling unexpected results, so we can define a new type for either in our app.
|
||||||
|
```ts
|
||||||
|
export type ApiEither<ResponseType> = Either<
|
||||||
|
BaseFailure<unknown>,
|
||||||
|
ResponseType
|
||||||
|
>;
|
||||||
|
```
|
||||||
|
So, for any API calls, we can use the Either type to handle both success and failure cases.
|
||||||
|
|
||||||
|
Additionally, for asynchronous processes, we use TaskEither, which works similarly to Either but is designed for handling asynchronous operations.
|
||||||
|
|
||||||
|
|
||||||
|
```ts
|
||||||
|
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, when creating a customer repository to handle all API calls for customers, we can use this type to manage success and failure cases.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default interface CustomerRepo {
|
||||||
|
fetchList(query: string): ApiTask<Customer[]>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
And in the repo we can have this pipe to get customer data:
|
||||||
|
|
||||||
|
> Note: In functional programming, a pipe is a composition of functions where the output of one function is passed as the input to the next, allowing for a clean, readable flow of data transformations.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
pipe(
|
||||||
|
tryCatch(
|
||||||
|
async () => {
|
||||||
|
...// calling api and returning result
|
||||||
|
},
|
||||||
|
(l) => failureOr(l, new NetworkFailure(l as Error)),
|
||||||
|
),
|
||||||
|
map(this.customersDto.bind(this)),
|
||||||
|
) as ApiTask<Customer[]>;
|
||||||
|
```
|
||||||
|
As you can see, in the try-catch block, which is the constructor of ApiEither, we define the right response in the first callback and the failure in the second callback argument.
|
||||||
|
|
||||||
|
failureOr is just a helper function that takes an error and converts it into a specific failure type, NetworkFailure in this example.
|
||||||
|
|
||||||
|
This ensures that during the process of fetching customer data, we always know the unexpected result will be of a specific type.
|
||||||
|
```ts
|
||||||
|
export function failureOr(
|
||||||
|
reason: unknown,
|
||||||
|
failure: BaseFailure<any>,
|
||||||
|
): BaseFailure<any> {
|
||||||
|
if (reason instanceof BaseFailure) {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
return failure;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
So in the process of fetching customer we know the unexpected result, always will be a speicfic type.
|
||||||
|
So in any layer we can get the failure do some logics on left response based on its metadata and turn the failure shape to any other failure shape and use it for different purposes.
|
||||||
|
|
||||||
|
## Usecases of this idea
|
||||||
|
|
||||||
|
There are many situations where, if an important process encounters problems, we want to have control over it. We need to know when and why these issues happened and store that information in one of the monitoring tools.
|
||||||
|
|
||||||
|
For example, when a CreateUserFailure occurs in the repository layer, we can send a log with the specific time and relevant parameter data to any logging or monitoring tool.
|
||||||
|
|
||||||
|
### Monitoring on bugs with dev failures
|
||||||
|
There are many situations, especially in frontend applications, where unexpected behavior occurs due to development mistakes or bugs. For example, when bugs or data changes in APIs happen, it's possible to face unexpected behaviors. In such cases, we want to show a specific message or redirect the user to an error page with a clear message.
|
||||||
|
|
||||||
|
Additionally, in frontend applications, logs may not be directly available in these situations, as the issue occurs on the user's system. To handle this, we can send metadata as a log to an API when encountering development failures.
|
||||||
|
|
||||||
|
To achieve this, we can simply define another abstract failure like this:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default abstract class BaseDevFailure<
|
||||||
|
META_DATA,
|
||||||
|
> extends BaseFailure<META_DATA> {}
|
||||||
|
```
|
||||||
|
As you can see, it’s just another failure that extends from the base failure.
|
||||||
|
|
||||||
|
For example, in some parts of the application, when sending dynamic arguments into the domain layer, there's a possibility of sending unexpected data. In such situations, we can define a specific development failure like this:
|
||||||
|
```ts
|
||||||
|
export default class ArgumentsFailure<
|
||||||
|
META_DATA,
|
||||||
|
> extends BaseDevFailure<META_DATA> {
|
||||||
|
constructor(metadata?: META_DATA) {
|
||||||
|
super(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
So we can consider this scenario in our logics and facing with this failure we can make a log request to our log api even from frontend applications, so on facing with this situation they can show a descent message to user to contact to support team at the same time they store the bug log to have full controll on these situations.
|
||||||
|
|
||||||
|
### Manage translations and error messages with failure
|
||||||
|
With this approach, we can go a step further than just error handling and even manage translations and display related messages in frontend applications automatically.
|
||||||
|
|
||||||
|
For each process and scenario, we should define a specific failure. At the same time, for each failure, we should display a corresponding message in the selected language based on the user's preference.
|
||||||
|
|
||||||
|
We can use this idea to automate both the error handling and message translation process.
|
||||||
|
|
||||||
|
To achieve this, we can pass a unique string key from the constructor based on the failure scenario. Our base failure will look like this:
|
||||||
|
```ts
|
||||||
|
export default abstract class BaseFailure<META_DATA> {
|
||||||
|
private readonly BASE_FAILURE_MESSAGE = "failure";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use this message as key lang for failure messages
|
||||||
|
*/
|
||||||
|
message = this.BASE_FAILURE_MESSAGE;
|
||||||
|
|
||||||
|
metadata: META_DATA | undefined;
|
||||||
|
|
||||||
|
constructor(key: string, metadata?: META_DATA) {
|
||||||
|
this.message = makeFailureMessage(this.message, key);
|
||||||
|
this.metadata = metadata ?? undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets Message key and it'll add it to the failure message key hierarchy
|
||||||
|
*/
|
||||||
|
export function makeFailureMessage(message: string, key: string) {
|
||||||
|
if (!key) return message;
|
||||||
|
return `${message}.${key}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
As you can see, we have a message property, which contains `BASE_FAILURE_MESSAGE`, the base key for all failure messages. It also accepts a key from the constructor, and with the makeFailureMessage function, it concatenates the new key with the message, shaping a unique message for each failure.
|
||||||
|
|
||||||
|
Each failure can have its own key passed from its constructor.
|
||||||
|
|
||||||
|
In the end, we can have a chained message key that we can use as the message key for each failure.
|
||||||
|
|
||||||
|
For example, for a failure like `UserAlreadyExistsFailure`, we can have a parent failure for all user domain failures, like this:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export default class UserFailure extends BaseFailure {
|
||||||
|
constructor(key: string) {
|
||||||
|
super(makeFailureMessage("user", key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
and now we can define our failure:
|
||||||
|
```ts
|
||||||
|
export default class UserAlreadyExistsFailure extends UserFailure {
|
||||||
|
constructor() {
|
||||||
|
super("alreadyExists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
so the result of message for `UserAlreadyExistsFailure`, will be `failure.user.alreadyExists`.
|
||||||
|
|
||||||
|
At the same time, in another part of our project, we're using a langKey object to specify the translation key. This object, like the failure structure, follows the domain and folder structure to specify the language key.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const langKey = {
|
||||||
|
// ...
|
||||||
|
failure: {
|
||||||
|
user: {
|
||||||
|
alreadyExists: "failure.user.alreadyExists",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
So, we can use our failure message key to retrieve the language key. By passing it to the translation method, we can get the translated failure message and automate the process of displaying the error message based on the failure we encounter.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const usecaseResponse = await getUsersUsecase() as Promise<Either<BaseFailure, User[]>>
|
||||||
|
|
||||||
|
if (!isLeft(usecaseResponse)) return;
|
||||||
|
if (!(usecaseResponse instanceOf BaseFailure)) return;
|
||||||
|
|
||||||
|
const translatedFailureMessage = t(usecaseResponse.left.message)
|
||||||
|
```
|
||||||
|
This is the final version, class diagram for our failur architecture:
|
||||||
|

|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
In this article, we've explored how to handle failures effectively in software applications by combining error handling with functional programming concepts like the Either type.
|
||||||
|
|
||||||
|
Furthermore, by integrating these failure handling mechanisms with automated processes for translating and displaying error messages, we create a more seamless experience for users, no matter the scenario. This approach not only improves the user experience by offering clear and context-specific messages, but it also provides valuable insights through monitoring and logging, enabling teams to quickly address issues.
|
||||||
|
|
||||||
|
Ultimately, this architecture supports building more robust, maintainable, and user-friendly applications, which I have used in many of my own projects, especially in frontend ones.
|
||||||
|
|
@ -1,13 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Button from "@/app/components/button/button";
|
|
||||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
|
||||||
import { useDI } from "@/bootstrap/di/di-context";
|
|
||||||
import { useRef } from "react";
|
|
||||||
|
|
||||||
export default function CreateRandomInvoiceContainer() {
|
|
||||||
const di = useDI();
|
|
||||||
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
|
|
||||||
|
|
||||||
return <Button vm={vm.current} />;
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
import { DocumentIcon } from "@/app/components/icons/document";
|
|
||||||
import HomeIcon from "@/app/components/icons/home";
|
|
||||||
import { UserIcon } from "@/app/components/icons/user";
|
|
||||||
import { usePathname } from "next/navigation";
|
|
||||||
|
|
||||||
type LinkItem = {
|
|
||||||
name: string;
|
|
||||||
href: string;
|
|
||||||
icon: (props: { className?: string }) => JSX.Element;
|
|
||||||
};
|
|
||||||
export default function navLinkPersonalVM() {
|
|
||||||
const pathname = usePathname();
|
|
||||||
// Map of links to display in the side navigation.
|
|
||||||
// Depending on the size of the application, this would be stored in a database.
|
|
||||||
const links: LinkItem[] = [
|
|
||||||
{ name: "Home", href: "/dashboard", icon: HomeIcon },
|
|
||||||
{
|
|
||||||
name: "Invoices",
|
|
||||||
href: "/dashboard/invoices",
|
|
||||||
icon: DocumentIcon,
|
|
||||||
},
|
|
||||||
{ name: "Customers", href: "/dashboard/customers", icon: UserIcon },
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
links,
|
|
||||||
isLinkActive: (link: LinkItem) => pathname === link.href,
|
|
||||||
};
|
|
||||||
}
|
|
@ -0,0 +1,29 @@
|
|||||||
|
import HomeIcon from "@/app/components/icons/home";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
type LinkItem = {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
icon: (props: { className?: string }) => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Beside of reusable vm each View can have it's own personal vm to handle it's ownlogics.
|
||||||
|
* Difference between personal vm and other vms which extends BaseVM, is that
|
||||||
|
* personal vm directly will be called inside of view and instinctly connected to the view,
|
||||||
|
* so they come together always and there is no need to be connected with interface for reusable
|
||||||
|
* vms.
|
||||||
|
*/
|
||||||
|
export default function navLinkPersonalVM() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
// Map of links to display in the side navigation.
|
||||||
|
// Depending on the size of the application, this would be stored in a database.
|
||||||
|
const links = useRef<LinkItem[]>([
|
||||||
|
{ name: "Home", href: "/dashboard", icon: HomeIcon },
|
||||||
|
]).current;
|
||||||
|
return {
|
||||||
|
links,
|
||||||
|
isLinkActive: (link: LinkItem) => pathname.includes(link.href),
|
||||||
|
};
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link-vm";
|
import navLinkPersonalVM from "@/app/[lang]/dashboard/components/client/nav-links/nav-link.personal-vm";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
"use client"
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
|
||||||
import * as React from "react"
|
"use client";
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,13 @@ import {
|
|||||||
InboxIcon,
|
InboxIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export default function cardController(props: {
|
/**
|
||||||
|
* Controllers are bridge between feature layer and application layer.
|
||||||
|
* They decide, feature layer will be cached or not, where to run in client or server
|
||||||
|
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||||
|
* client state managements.
|
||||||
|
*/
|
||||||
|
export default function getCardIcon(props: {
|
||||||
type: "invoices" | "customers" | "pending" | "collected";
|
type: "invoices" | "customers" | "pending" | "collected";
|
||||||
}) {
|
}) {
|
||||||
const { type } = props;
|
const { type } = props;
|
@ -1,4 +1,4 @@
|
|||||||
import cardController from "@/app/[lang]/dashboard/components/server/card/card-controller";
|
import getCardIcon from "@/app/[lang]/dashboard/components/server/card-icon";
|
||||||
|
|
||||||
export function Card({
|
export function Card({
|
||||||
title,
|
title,
|
||||||
@ -9,7 +9,7 @@ export function Card({
|
|||||||
value: number | string;
|
value: number | string;
|
||||||
type: "invoices" | "customers" | "pending" | "collected";
|
type: "invoices" | "customers" | "pending" | "collected";
|
||||||
}) {
|
}) {
|
||||||
const { Icon } = cardController({ type });
|
const { Icon } = getCardIcon({ type });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl bg-gray-50 p-2 shadow-sm">
|
<div className="rounded-xl bg-gray-50 p-2 shadow-sm">
|
@ -1,9 +1,9 @@
|
|||||||
import { Card } from "@/app/[lang]/dashboard/components/server/card/card";
|
import { Card } from "@/app/[lang]/dashboard/components/server/card";
|
||||||
import cardsController from "@/app/[lang]/dashboard/components/server/cards/cards-controller";
|
import fetchSummaryInfoController from "@/app/[lang]/dashboard/controller/fetch-summary-info.controller";
|
||||||
|
|
||||||
export default async function CardWrapper() {
|
export default async function CardWrapper() {
|
||||||
const { customersNumber, invoicesNumber, invoicesSummary } =
|
const { customersNumber, invoicesNumber, invoicesSummary } =
|
||||||
await cardsController();
|
await fetchSummaryInfoController();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
@ -1,7 +0,0 @@
|
|||||||
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info-usecase";
|
|
||||||
import { connection } from "next/server";
|
|
||||||
|
|
||||||
export default function cardsController() {
|
|
||||||
connection();
|
|
||||||
return fetchSummaryInfoUsecase();
|
|
||||||
}
|
|
@ -1,5 +1,6 @@
|
|||||||
import CreateRandomInvoiceContainer from "@/app/[lang]/dashboard/components/client/create-random-invoice/create-random-invoice";
|
import latestInvoicesController from "@/app/[lang]/dashboard/controller/latest-invoices.controller";
|
||||||
import latestInvoicesController from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices-controller";
|
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||||
|
import Button from "@/app/components/button/button";
|
||||||
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
import { ArrowPathIcon } from "@heroicons/react/24/outline";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { isLeft } from "fp-ts/lib/Either";
|
import { isLeft } from "fp-ts/lib/Either";
|
||||||
@ -39,6 +40,7 @@ export default async function LatestInvoices() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col md:col-span-4">
|
<div className="flex w-full flex-col md:col-span-4">
|
||||||
<h2 className="mb-4 text-xl md:text-2xl">Latest Invoices</h2>
|
<h2 className="mb-4 text-xl md:text-2xl">Latest Invoices</h2>
|
||||||
@ -48,7 +50,7 @@ export default async function LatestInvoices() {
|
|||||||
<ArrowPathIcon className="h-5 w-5 text-gray-500" />
|
<ArrowPathIcon className="h-5 w-5 text-gray-500" />
|
||||||
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
|
<h3 className="ml-2 text-sm text-gray-500 ">Updated just now</h3>
|
||||||
</div>
|
</div>
|
||||||
<CreateRandomInvoiceContainer />
|
<Button vmKey={CreateRandomInvoiceButtonVM} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
@ -1,7 +0,0 @@
|
|||||||
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices-usecase";
|
|
||||||
import { connection } from "next/server";
|
|
||||||
|
|
||||||
export default function latestInvoicesController() {
|
|
||||||
connection();
|
|
||||||
return fetchCustomerInvoicesUsecase();
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
import revenueChartController from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart-controller";
|
import revenueChartController from "@/app/[lang]/dashboard/controller/revenue-chart.controller";
|
||||||
import { CalendarIcon } from "@heroicons/react/24/outline";
|
import { CalendarIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export default async function RevenueChart() {
|
export default async function RevenueChart() {
|
@ -0,0 +1,27 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { ApiEither } from "@/feature/common/data/api-task";
|
||||||
|
import serverDi from "@/feature/common/server.di";
|
||||||
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||||
|
import {
|
||||||
|
CreateInvoiceUsecase,
|
||||||
|
createInvoiceUsecaseKey,
|
||||||
|
} from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
|
||||||
|
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||||
|
import { connection } from "next/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controllers are bridge between feature layer and application layer.
|
||||||
|
* They decide, feature layer will be cached or not, where to run in client or server
|
||||||
|
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||||
|
* client state managements.
|
||||||
|
*/
|
||||||
|
export default async function createInvoiceController(
|
||||||
|
params: InvoiceParam,
|
||||||
|
): Promise<ApiEither<string>> {
|
||||||
|
connection();
|
||||||
|
const usecase = serverDi(invoiceModuleKey).resolve<CreateInvoiceUsecase>(
|
||||||
|
createInvoiceUsecaseKey,
|
||||||
|
);
|
||||||
|
return usecase(params);
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import fetchSummaryInfoUsecase from "@/feature/core/summary-info/domain/usecase/fetch-summary-info.usecase";
|
||||||
|
import { connection } from "next/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controllers are bridge between feature layer and application layer.
|
||||||
|
* They decide, feature layer will be cached or not, where to run in client or server
|
||||||
|
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||||
|
* client state managements.
|
||||||
|
*/
|
||||||
|
export default function fetchSummaryInfoController() {
|
||||||
|
connection();
|
||||||
|
return fetchSummaryInfoUsecase();
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
import fetchCustomerInvoicesUsecase from "@/feature/core/customer-invoice/domain/usecase/fetch-customer-invoices.usecase";
|
||||||
|
import { connection } from "next/server";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controllers are bridge between feature layer and application layer.
|
||||||
|
* They decide, feature layer will be cached or not, where to run in client or server
|
||||||
|
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||||
|
* client state managements.
|
||||||
|
*/
|
||||||
|
export default function latestInvoicesController() {
|
||||||
|
connection();
|
||||||
|
return fetchCustomerInvoicesUsecase();
|
||||||
|
}
|
@ -1,6 +1,12 @@
|
|||||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||||
import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues-usecase";
|
import fetchRevenuesUsecase from "@/feature/core/revenue/domain/usecase/fetch-revenues.usecase";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controllers are bridge between feature layer and application layer.
|
||||||
|
* They decide, feature layer will be cached or not, where to run in client or server
|
||||||
|
* Or connect multiple usecases and run them, handle their failure, hydrate and store data in
|
||||||
|
* client state managements.
|
||||||
|
*/
|
||||||
export default async function revenueChartController() {
|
export default async function revenueChartController() {
|
||||||
const revenue = await fetchRevenuesUsecase();
|
const revenue = await fetchRevenuesUsecase();
|
||||||
const chartHeight = 350;
|
const chartHeight = 350;
|
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import SideNav from "@/app/[lang]/dashboard/components/server/sidenav";
|
import SideNav from "@/app/[lang]/dashboard/components/server/sidenav";
|
||||||
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard-app-module";
|
import dashboardAppModule from "@/app/[lang]/dashboard/module/dashboard.app-module";
|
||||||
import { DiContext } from "@/bootstrap/di/di-context";
|
import { DiContext } from "@/bootstrap/di/di-context";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
|
import DashboardSkeleton from "@/app/[lang]/dashboard/components/server/skeletons";
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return <DashboardSkeleton />;
|
return <DashboardSkeleton />;
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
|
import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller";
|
||||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
||||||
import di from "@/bootstrap/di/init-di";
|
import di from "@/bootstrap/di/init-di";
|
||||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Each page can have its own di to connect all vms, usecases or controllers
|
||||||
|
*/
|
||||||
export default function dashboardAppModule() {
|
export default function dashboardAppModule() {
|
||||||
const dashboardDi = di.createChildContainer();
|
const dashboardDi = di.createChildContainer();
|
||||||
|
|
||||||
dashboardDi.register(createInvoiceUsecase.name, {
|
|
||||||
useValue: createInvoiceUsecase,
|
|
||||||
});
|
|
||||||
dashboardDi.register(
|
dashboardDi.register(
|
||||||
CreateRandomInvoiceButtonVM,
|
CreateRandomInvoiceButtonVM.name,
|
||||||
CreateRandomInvoiceButtonVM,
|
CreateRandomInvoiceButtonVM,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
dashboardDi.register(createInvoiceController.name, {
|
||||||
|
useValue: createInvoiceController,
|
||||||
|
});
|
||||||
return dashboardDi;
|
return dashboardDi;
|
||||||
}
|
}
|
@ -1,28 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
LatestInvoicesSkeleton,
|
LatestInvoicesSkeleton,
|
||||||
RevenueChartSkeleton,
|
RevenueChartSkeleton,
|
||||||
} from "@/app/[lang]/dashboard/components/server/skeletons/skeletons";
|
} from "@/app/[lang]/dashboard/components/server/skeletons";
|
||||||
import CardWrapper from "@/app/[lang]/dashboard/components/server/cards/cards";
|
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart";
|
||||||
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices/latest-invoices";
|
|
||||||
import RevenueChart from "@/app/[lang]/dashboard/components/server/revenue-chart/revenue-chart";
|
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { getServerTranslation, LANGS } from "@/bootstrap/i18n/i18n";
|
import LatestInvoices from "@/app/[lang]/dashboard/components/server/latest-invoices";
|
||||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
|
||||||
|
|
||||||
export default async function Dashboard(props: {
|
export default async function Dashboard() {
|
||||||
params: Promise<{ lang: LANGS }>;
|
|
||||||
}) {
|
|
||||||
const { params } = props;
|
|
||||||
const { lang } = await params;
|
|
||||||
const { t } = await getServerTranslation(lang);
|
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<h1 className="mb-4 text-xl md:text-2xl">
|
|
||||||
{t(langKey.global.dashboard)}
|
|
||||||
</h1>
|
|
||||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
||||||
<CardWrapper />
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
|
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
|
||||||
<Suspense fallback={<RevenueChartSkeleton />}>
|
<Suspense fallback={<RevenueChartSkeleton />}>
|
||||||
<RevenueChart />
|
<RevenueChart />
|
||||||
|
@ -1,20 +1,27 @@
|
|||||||
import ButtonVm from "@/app/components/button/button-vm";
|
"use client";
|
||||||
|
|
||||||
|
import createInvoiceController from "@/app/[lang]/dashboard/controller/create-invoice.controller";
|
||||||
|
import ButtonVm from "@/app/components/button/button.i-vm";
|
||||||
import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action";
|
import { useServerAction } from "@/bootstrap/helpers/hooks/use-server-action";
|
||||||
import useThrottle from "@/bootstrap/helpers/hooks/use-throttle";
|
import useThrottle from "@/bootstrap/helpers/hooks/use-throttle";
|
||||||
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
|
import BaseVM from "@/bootstrap/helpers/vm/base-vm";
|
||||||
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Viewmodel for the button view to connect to business logics and all UI logics
|
||||||
|
* For UI logics, all translations, states, sideeffects and events will be handled
|
||||||
|
* in this layer.
|
||||||
|
*/
|
||||||
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
|
export default class CreateRandomInvoiceButtonVM extends BaseVM<ButtonVm> {
|
||||||
private createInvoice: typeof createInvoiceUsecase;
|
private createInvoice: typeof createInvoiceController;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.createInvoice = this.di.resolve(createInvoiceUsecase.name);
|
this.createInvoice = this.di.resolve(createInvoiceController.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
useVM(): ButtonVm {
|
useVM(): ButtonVm {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { ThemeProvider } from "@/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
|
import { ThemeProvider } from "@/app/[lang]/dashboard/components/client/theme-provider/theme-provider";
|
||||||
import { initI18next, LANGS } from "@/bootstrap/i18n/i18n";
|
import { getI18n, LANGS } from "@/bootstrap/i18n/i18n";
|
||||||
import TranslationsProvider from "@/bootstrap/i18n/i18n-provider";
|
import TranslationsProvider from "@/bootstrap/i18n/i18n-provider";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import { PropsWithChildren } from "react";
|
import { PropsWithChildren } from "react";
|
||||||
@ -20,7 +20,7 @@ export default async function layout(
|
|||||||
) {
|
) {
|
||||||
const { params, children } = props;
|
const { params, children } = props;
|
||||||
const { lang } = await params;
|
const { lang } = await params;
|
||||||
const { resources } = await initI18next({ lng: lang });
|
await getI18n({ lng: lang });
|
||||||
return (
|
return (
|
||||||
<html lang={lang} suppressHydrationWarning>
|
<html lang={lang} suppressHydrationWarning>
|
||||||
<body
|
<body
|
||||||
@ -32,9 +32,7 @@ export default async function layout(
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
<TranslationsProvider lng={lang} resources={resources}>
|
<TranslationsProvider lng={lang}>{children}</TranslationsProvider>
|
||||||
{children}
|
|
||||||
</TranslationsProvider>
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -1,4 +1,13 @@
|
|||||||
export default function Home() {
|
import langKey from "@/bootstrap/i18n/dictionaries/lang-key";
|
||||||
|
import { getServerTranslation, LANGS } from "@/bootstrap/i18n/i18n";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export default async function Home(props: {
|
||||||
|
params: Promise<{ lang: LANGS }>;
|
||||||
|
}) {
|
||||||
|
const { params } = props;
|
||||||
|
const { lang } = await params;
|
||||||
|
const { t } = await getServerTranslation(lang);
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen flex-col p-6">
|
<main className="flex min-h-screen flex-col p-6">
|
||||||
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
|
<div className="mt-4 flex grow flex-col gap-4 md:flex-row">
|
||||||
@ -8,6 +17,12 @@ export default function Home() {
|
|||||||
<strong>Welcome to Acme.</strong> This is the example for the ,
|
<strong>Welcome to Acme.</strong> This is the example for the ,
|
||||||
brought to you by Vercel.
|
brought to you by Vercel.
|
||||||
</p>
|
</p>
|
||||||
|
<Link
|
||||||
|
className="flex rounded-md bg-primary-foreground p-3 ml-auto mr-auto text-white"
|
||||||
|
href="dashboard"
|
||||||
|
>
|
||||||
|
{t(langKey.global.dashboard)}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
export default interface ButtonVm {
|
|
||||||
props: {
|
|
||||||
title: string;
|
|
||||||
isDisable: boolean;
|
|
||||||
};
|
|
||||||
onClick(): void;
|
|
||||||
}
|
|
11
src/app/components/button/button.i-vm.ts
Normal file
11
src/app/components/button/button.i-vm.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Interface for viewmodel of button. this is bridge between view and viewmodel.
|
||||||
|
* With this interface, view is adapter and vms will be implementations in bridge pattern
|
||||||
|
*/
|
||||||
|
export default interface ButtonVm {
|
||||||
|
props: {
|
||||||
|
title: string;
|
||||||
|
isDisable: boolean;
|
||||||
|
};
|
||||||
|
onClick(): void;
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
|
import BaseView, { BuildProps } from "@/bootstrap/helpers/view/base-view";
|
||||||
import ButtonVm from "@/app/components/button/button-vm";
|
import ButtonVm from "@/app/components/button/button.i-vm";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
import CreateRandomInvoiceButtonVM from "@/app/[lang]/dashboard/vm/create-random-invoice-button-vm";
|
|
||||||
import Button from "@/app/components/button/button";
|
import Button from "@/app/components/button/button";
|
||||||
import { DiContext, useDI } from "@/bootstrap/di/di-context";
|
|
||||||
import mockedModuleDi from "@/bootstrap/di/mocked-module-di";
|
|
||||||
import Story from "@/bootstrap/helpers/view/storybook-base-template-type";
|
import Story from "@/bootstrap/helpers/view/storybook-base-template-type";
|
||||||
import getArgVM from "@/bootstrap/helpers/view/storybook-with-arg-vm";
|
import getArgVM from "@/bootstrap/helpers/view/storybook-with-arg-vm";
|
||||||
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice-usecase";
|
|
||||||
import type { Meta } from "@storybook/react";
|
import type { Meta } from "@storybook/react";
|
||||||
import { useRef } from "react";
|
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: "general/Button",
|
title: "general/Button",
|
||||||
@ -32,34 +27,3 @@ export const Primary: Story = {
|
|||||||
return <Button vm={vm} memoizedByVM={false} />;
|
return <Button vm={vm} memoizedByVM={false} />;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WithVM: Story = {
|
|
||||||
decorators: [
|
|
||||||
(Story) => {
|
|
||||||
const di = mockedModuleDi([
|
|
||||||
{
|
|
||||||
token: CreateRandomInvoiceButtonVM,
|
|
||||||
provider: CreateRandomInvoiceButtonVM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
token: createInvoiceUsecase.name,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, no-console
|
|
||||||
provider: (args: any) => console.log("clicked", args),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
return <Story di={di} />;
|
|
||||||
},
|
|
||||||
],
|
|
||||||
render: (_, globalProps) => {
|
|
||||||
function Child() {
|
|
||||||
const di = useDI();
|
|
||||||
const vm = useRef(di.resolve(CreateRandomInvoiceButtonVM));
|
|
||||||
return <Button vm={vm.current} memoizedByVM={false} />;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<DiContext.Provider value={globalProps.di}>
|
|
||||||
<Child />
|
|
||||||
</DiContext.Provider>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
@ -2,6 +2,9 @@ import { constructor } from "tsyringe/dist/typings/types";
|
|||||||
|
|
||||||
export const isServer = typeof window === "undefined";
|
export const isServer = typeof window === "undefined";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given value is a class
|
||||||
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function isClass(fn: any): fn is constructor<unknown> {
|
export function isClass(fn: any): fn is constructor<unknown> {
|
||||||
return typeof fn === "function" && /^(class|function [A-Z])/.test(fn);
|
return typeof fn === "function" && /^(class|function [A-Z])/.test(fn);
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { clsx, type ClassValue } from "clsx";
|
import { clsx, type ClassValue } from "clsx";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To connect tailwind classes.
|
||||||
|
* @param inputs Tailwind classes
|
||||||
|
*/
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs));
|
return twMerge(clsx(inputs));
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
"use client";
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
|
|
||||||
// import gdi from "@/bootstrap/di/init-di";
|
|
||||||
/* eslint-disable react/display-name */
|
/* eslint-disable react/display-name */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
|
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useDI } from "@/bootstrap/di/di-context";
|
||||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||||
import { Component, ReactNode, FC, PropsWithChildren, memo } from "react";
|
import { Component, ReactNode, FC, PropsWithChildren, memo } from "react";
|
||||||
|
import { InjectionToken } from "tsyringe";
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
/* Connector Component */
|
/* Connector Component */
|
||||||
@ -23,7 +26,6 @@ interface IVvmConnector<IVM, PROPS> extends PropsWithChildren {
|
|||||||
const VvmConnector = memo(
|
const VvmConnector = memo(
|
||||||
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
|
<IVM, PROPS>(props: IVvmConnector<IVM, PROPS>) => {
|
||||||
const { View, Vm, restProps, children } = props;
|
const { View, Vm, restProps, children } = props;
|
||||||
|
|
||||||
const vm = Vm.useVM();
|
const vm = Vm.useVM();
|
||||||
|
|
||||||
const allProps = {
|
const allProps = {
|
||||||
@ -45,8 +47,7 @@ const VvmConnector = memo(
|
|||||||
type IVMParent = Record<string, any>;
|
type IVMParent = Record<string, any>;
|
||||||
type IPropParent = Record<string, any> | undefined;
|
type IPropParent = Record<string, any> | undefined;
|
||||||
|
|
||||||
type BaseProps<IVM extends IVMParent, PROPS extends IPropParent = undefined> = {
|
type BaseProps<PROPS extends IPropParent = undefined> = {
|
||||||
vm: IBaseVM<IVM>;
|
|
||||||
restProps?: PROPS;
|
restProps?: PROPS;
|
||||||
/**
|
/**
|
||||||
* By default it's true.
|
* By default it's true.
|
||||||
@ -57,6 +58,24 @@ type BaseProps<IVM extends IVMParent, PROPS extends IPropParent = undefined> = {
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type BasePropsWithVM<
|
||||||
|
IVM extends IVMParent,
|
||||||
|
PROPS extends IPropParent = undefined,
|
||||||
|
> = BaseProps<PROPS> & {
|
||||||
|
/**
|
||||||
|
* Directly instantiated vm
|
||||||
|
*/
|
||||||
|
vm: IBaseVM<IVM>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BasePropsWithVMKey<PROPS extends IPropParent = undefined> =
|
||||||
|
BaseProps<PROPS> & {
|
||||||
|
/**
|
||||||
|
* TSyringe key for vm to be injected
|
||||||
|
*/
|
||||||
|
vmKey: InjectionToken;
|
||||||
|
};
|
||||||
|
|
||||||
export type BuildProps<
|
export type BuildProps<
|
||||||
IVM extends IVMParent,
|
IVM extends IVMParent,
|
||||||
PROPS extends IPropParent = undefined,
|
PROPS extends IPropParent = undefined,
|
||||||
@ -66,10 +85,36 @@ export type BuildProps<
|
|||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ViewProps<
|
||||||
|
IVM extends IVMParent,
|
||||||
|
PROPS extends IPropParent = undefined,
|
||||||
|
> = BasePropsWithVM<IVM, PROPS> | BasePropsWithVMKey<PROPS>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base view is base component for all views in mvvm architecture which gets
|
||||||
|
* vm as props and connect it to the view and memoize the component by default
|
||||||
|
* to just render just on changes of its vm.
|
||||||
|
*/
|
||||||
export default abstract class BaseView<
|
export default abstract class BaseView<
|
||||||
IVM extends IVMParent,
|
IVM extends IVMParent,
|
||||||
PROPS extends IPropParent = undefined,
|
PROPS extends IPropParent = undefined,
|
||||||
> extends Component<BaseProps<IVM, PROPS>> {
|
> extends Component<ViewProps<IVM, PROPS>> {
|
||||||
|
private vm: IBaseVM<IVM> | undefined;
|
||||||
|
|
||||||
|
constructor(props: ViewProps<IVM, PROPS>) {
|
||||||
|
super(props);
|
||||||
|
this.vm = this.initVm;
|
||||||
|
}
|
||||||
|
|
||||||
|
private get initVm() {
|
||||||
|
if (Object.hasOwn(this.props, "vmKey")) {
|
||||||
|
const { vmKey } = this.props as BasePropsWithVMKey<PROPS>;
|
||||||
|
const di = useDI();
|
||||||
|
return di.resolve(vmKey) as IBaseVM<IVM>;
|
||||||
|
}
|
||||||
|
return (this.props as BasePropsWithVM<IVM, PROPS>).vm;
|
||||||
|
}
|
||||||
|
|
||||||
protected get componentName() {
|
protected get componentName() {
|
||||||
return this.constructor.name;
|
return this.constructor.name;
|
||||||
}
|
}
|
||||||
@ -77,9 +122,16 @@ export default abstract class BaseView<
|
|||||||
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
|
protected abstract Build(props: BuildProps<IVM, PROPS>): ReactNode;
|
||||||
|
|
||||||
render(): ReactNode {
|
render(): ReactNode {
|
||||||
const { vm, restProps, memoizedByVM, children, ...rest } = this.props;
|
const { restProps, memoizedByVM, children, ...rest } = this.props;
|
||||||
|
|
||||||
VvmConnector.displayName = this.componentName;
|
VvmConnector.displayName = this.componentName;
|
||||||
|
const vm = memoizedByVM ? this.vm : this.initVm;
|
||||||
|
if (!vm) {
|
||||||
|
const isVmKey = Object.hasOwn(this.props, "vmKey");
|
||||||
|
const message = isVmKey
|
||||||
|
? "vm is not defined, check your di configuration"
|
||||||
|
: "pass correct vm";
|
||||||
|
throw new Error(`Vm is not defined${message}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VvmConnector
|
<VvmConnector
|
||||||
@ -92,5 +144,4 @@ export default abstract class BaseView<
|
|||||||
</VvmConnector>
|
</VvmConnector>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To use with mvvm library to make a vm based on props so you can pass the result to the view
|
||||||
|
* @param vmObj All properties which view needs to get from vm.
|
||||||
|
* @returns Vm which is suitable to be passed to the view
|
||||||
|
* @example const vm = getArgVM(globalData.parsedProps.vm);
|
||||||
|
return <Button vm={vm} memoizedByVM={false} />;
|
||||||
|
*/
|
||||||
const getArgVM = <IVM>(vmObj: IVM) => {
|
const getArgVM = <IVM>(vmObj: IVM) => {
|
||||||
class VM implements IBaseVM<IVM> {
|
class VM implements IBaseVM<IVM> {
|
||||||
useVM(): IVM {
|
useVM(): IVM {
|
||||||
|
@ -5,6 +5,12 @@ import { NoOverride } from "@/bootstrap/helpers/type-helper";
|
|||||||
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
import IBaseVM from "@/bootstrap/helpers/vm/i-base-vm";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class for all viewmodels. It provides
|
||||||
|
* - dependency injection: To get closes di which serves from di provider
|
||||||
|
* - rerender method: to rerender your component manually
|
||||||
|
* - produce method: to produce your vm dynamically by passing and attaching dependencies to it
|
||||||
|
*/
|
||||||
export default abstract class BaseVM<
|
export default abstract class BaseVM<
|
||||||
IVM,
|
IVM,
|
||||||
DEP extends object | undefined = undefined,
|
DEP extends object | undefined = undefined,
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* All viewmodels should implement this interface.
|
||||||
|
*/
|
||||||
export default interface IBaseVM<VM> {
|
export default interface IBaseVM<VM> {
|
||||||
useVM(): VM;
|
useVM(): VM;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* main language keys which will be used for translation to avoid using strings directly and be
|
||||||
|
* a single source of truth in all changes between all languages dictionaries.
|
||||||
|
* All languages dictionaries should have the same keys by having this object type.
|
||||||
|
*/
|
||||||
const langKey = {
|
const langKey = {
|
||||||
global: {
|
global: {
|
||||||
home: "global.home",
|
home: "global.home",
|
||||||
|
@ -1,17 +1,24 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
import { i18nInstance, initI18next, LANGS } from "@/bootstrap/i18n/i18n";
|
import { getI18n, LANGS } from "@/bootstrap/i18n/i18n";
|
||||||
import { Resource } from "i18next";
|
import { PropsWithChildren, useEffect, useState } from "react";
|
||||||
import { PropsWithChildren } from "react";
|
import { i18n } from "i18next";
|
||||||
|
import storeLang from "@/bootstrap/i18n/store-lang-action";
|
||||||
|
|
||||||
export default function TranslationsProvider({
|
export default function TranslationsProvider({
|
||||||
children,
|
children,
|
||||||
lng,
|
lng,
|
||||||
resources,
|
}: PropsWithChildren & { lng: LANGS }) {
|
||||||
}: PropsWithChildren & { lng: LANGS; resources: Resource }) {
|
const [i18n, setI18n] = useState<i18n>();
|
||||||
if (!resources) return children;
|
|
||||||
initI18next({ lng, resources });
|
|
||||||
|
|
||||||
return <I18nextProvider i18n={i18nInstance}>{children}</I18nextProvider>;
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
storeLang(lng);
|
||||||
|
setI18n((await getI18n({ lng })).i18n);
|
||||||
|
})();
|
||||||
|
}, [lng]);
|
||||||
|
|
||||||
|
if (!i18n) return null;
|
||||||
|
return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>;
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,7 @@ export enum LANGS {
|
|||||||
RU = "ru",
|
RU = "ru",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initI18next = async (params: {
|
export const getI18n = async (params: {
|
||||||
lng: LANGS;
|
lng: LANGS;
|
||||||
resources?: Resource;
|
resources?: Resource;
|
||||||
ns?: string;
|
ns?: string;
|
||||||
@ -43,13 +43,9 @@ export async function getServerTranslation(
|
|||||||
ns?: string,
|
ns?: string,
|
||||||
options: { keyPrefix?: string } = {},
|
options: { keyPrefix?: string } = {},
|
||||||
) {
|
) {
|
||||||
await initI18next({ lng });
|
const { i18n } = await getI18n({ lng });
|
||||||
return {
|
return {
|
||||||
t: i18nInstance.getFixedT(
|
t: i18n.getFixedT(lng, Array.isArray(ns) ? ns[0] : ns, options?.keyPrefix),
|
||||||
lng,
|
|
||||||
Array.isArray(ns) ? ns[0] : ns,
|
|
||||||
options?.keyPrefix,
|
|
||||||
),
|
|
||||||
i18n: i18nInstance,
|
i18n: i18nInstance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
9
src/bootstrap/i18n/store-lang-action.ts
Normal file
9
src/bootstrap/i18n/store-lang-action.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { LANGS } from "@/bootstrap/i18n/i18n";
|
||||||
|
import { cookieName } from "@/bootstrap/i18n/settings";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
|
||||||
|
export default async function storeLang(lng: LANGS) {
|
||||||
|
(await cookies()).set(cookieName, lng);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { Either } from "fp-ts/lib/Either";
|
import { Either } from "fp-ts/lib/Either";
|
||||||
import { TaskEither } from "fp-ts/lib/TaskEither";
|
import { TaskEither } from "fp-ts/lib/TaskEither";
|
||||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
import BaseFailure from "@/feature/common/failures/base.failure";
|
||||||
|
|
||||||
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
|
type ApiTask<ResponseType> = TaskEither<BaseFailure<unknown>, ResponseType>;
|
||||||
export type ApiEither<ResponseType> = Either<
|
export type ApiEither<ResponseType> = Either<
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import { makeFailureMessage } from "@/feature/common/failures/failure-helpers";
|
import { makeFailureMessage } from "@/feature/common/failures/failure-helpers";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a class called BaseFailure that extends the Error class. It is
|
* This class can be used as a base class for creating custom failure classes.
|
||||||
* used as a base class for creating custom failure classes.
|
* With this class you can set message and metadata, with messages and extending
|
||||||
|
* you can create your failure messages hierarchy and automatically by syncing langKey
|
||||||
|
* with the hirerarchy of failure messages.
|
||||||
|
* For example if you pass a key of `user` to the constructor of `UserCreationFailure`
|
||||||
|
* so in langKey you can have failure message `faiure.user` so automatically,
|
||||||
|
* you can show translated error message everywhere in the app.
|
||||||
|
* Also you can use this failure message to have grained control over failures.
|
||||||
*/
|
*/
|
||||||
export default abstract class BaseFailure<META_DATA> {
|
export default abstract class BaseFailure<META_DATA> {
|
||||||
/* ------------------------------- Attributes ------------------------------- */
|
/* ------------------------------- Attributes ------------------------------- */
|
@ -1,4 +1,4 @@
|
|||||||
import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
|
import BaseDevFailure from "@/feature/common/failures/dev/base-dev.failure";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Failure for needed arguments in a method but sent wrong one
|
* Failure for needed arguments in a method but sent wrong one
|
@ -1,5 +0,0 @@
|
|||||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
|
||||||
|
|
||||||
export default abstract class BaseDevFailure<
|
|
||||||
META_DATA,
|
|
||||||
> extends BaseFailure<META_DATA> {}
|
|
10
src/feature/common/failures/dev/base-dev.failure.ts
Normal file
10
src/feature/common/failures/dev/base-dev.failure.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import BaseFailure from "@/feature/common/failures/base.failure";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a base class for development failures. All dev failures means we as a developer
|
||||||
|
* made a mistake in the process and we should fix it and can be used in monitoring and
|
||||||
|
* should be handled in hotfix ASAP.
|
||||||
|
*/
|
||||||
|
export default abstract class BaseDevFailure<
|
||||||
|
META_DATA,
|
||||||
|
> extends BaseFailure<META_DATA> {}
|
@ -1,7 +1,7 @@
|
|||||||
import BaseDevFailure from "@/feature/common/failures/dev/base-dev-failure";
|
import BaseDevFailure from "@/feature/common/failures/dev/base-dev.failure";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a failure of not having specific dependency
|
* This is a failure when we didn't provice specific dependency.
|
||||||
*/
|
*/
|
||||||
export default class DependencyFailure<
|
export default class DependencyFailure<
|
||||||
META_DATA,
|
META_DATA,
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import BaseFailure from "@/feature/common/failures/base-failure";
|
import BaseFailure from "@/feature/common/failures/base.failure";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is supposed to save previous failure of TaskEither
|
* This method is supposed to save previous failure of TaskEither
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import BaseFailure from "./base-failure";
|
import BaseFailure from "./base.failure";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Failure for HTTP response when response dosn't have base structure
|
* Failure for HTTP response when response dosn't have base structure
|
@ -1,7 +1,7 @@
|
|||||||
import BaseFailure from "./base-failure";
|
import BaseFailure from "./base.failure";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Failure for params failure
|
* Failure for params failure. which means some params are missing or not valid
|
||||||
*/
|
*/
|
||||||
export default class ParamsFailure<META_DATA> extends BaseFailure<META_DATA> {
|
export default class ParamsFailure<META_DATA> extends BaseFailure<META_DATA> {
|
||||||
/* ------------------------------- Constructor ------------------------------ */
|
/* ------------------------------- Constructor ------------------------------ */
|
@ -1,14 +1,14 @@
|
|||||||
import getCustomerInvoiceDi from "@/feature/core/customer-invoice/data/module/customer-invoice-di";
|
import getCustomerInvoiceDi from "@/feature/core/customer-invoice/data/module/customer-invoice.di";
|
||||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
|
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice.module-key";
|
||||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||||
import getCustomerDi from "@/feature/core/customer/data/module/customer-di";
|
import getCustomerDi from "@/feature/core/customer/data/module/customer-di";
|
||||||
import getInvoiceDi from "@/feature/core/invoice/data/module/invoice-di";
|
import getInvoiceDi from "@/feature/core/invoice/data/module/invoice.di";
|
||||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||||
import { DependencyContainer } from "tsyringe";
|
import { DependencyContainer } from "tsyringe";
|
||||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info-module-key";
|
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info.module-key";
|
||||||
import getSummaryInfoDi from "@/feature/core/summary-info/data/module/summary-info-di";
|
import getSummaryInfoDi from "@/feature/core/summary-info/data/module/summary-info.di";
|
||||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue.module-key";
|
||||||
import getRevenueDi from "@/feature/core/revenue/data/module/revenue-di";
|
import getRevenueDi from "@/feature/core/revenue/data/module/revenue.di";
|
||||||
|
|
||||||
const memoizedDis: Record<string, DependencyContainer> = {};
|
const memoizedDis: Record<string, DependencyContainer> = {};
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import di from "@/bootstrap/di/init-di";
|
import di from "@/bootstrap/di/init-di";
|
||||||
import CustomerInvoiceDbRepo from "@/feature/core/customer-invoice/data/repo/customer-invoice-db-repo";
|
import CustomerInvoiceDbRepo from "@/feature/core/customer-invoice/data/repo/customer-invoice-db.repo";
|
||||||
import { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
import { customerInvoiceRepoKey } from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice.repo";
|
||||||
import { DependencyContainer } from "tsyringe";
|
import { DependencyContainer } from "tsyringe";
|
||||||
|
|
||||||
export default function getCustomerInvoiceDi(): DependencyContainer {
|
export default function getCustomerInvoiceDi(): DependencyContainer {
|
@ -1,10 +1,10 @@
|
|||||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||||
import ApiTask from "@/feature/common/data/api-task";
|
import ApiTask from "@/feature/common/data/api-task";
|
||||||
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
||||||
import NetworkFailure from "@/feature/common/failures/network-failure";
|
import NetworkFailure from "@/feature/common/failures/network.failure";
|
||||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
|
||||||
import CustomerInvoiceRepo from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
import CustomerInvoiceRepo from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice.repo";
|
||||||
import { pipe } from "fp-ts/lib/function";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
import { tryCatch } from "fp-ts/lib/TaskEither";
|
import { tryCatch } from "fp-ts/lib/TaskEither";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
@ -1,5 +1,5 @@
|
|||||||
import ApiTask from "@/feature/common/data/api-task";
|
import ApiTask from "@/feature/common/data/api-task";
|
||||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
|
||||||
|
|
||||||
export default interface CustomerInvoiceRepo {
|
export default interface CustomerInvoiceRepo {
|
||||||
fetchList(): ApiTask<CustomerInvoice[]>;
|
fetchList(): ApiTask<CustomerInvoice[]>;
|
@ -1,11 +1,10 @@
|
|||||||
import "server-only";
|
|
||||||
import { ApiEither } from "@/feature/common/data/api-task";
|
import { ApiEither } from "@/feature/common/data/api-task";
|
||||||
import serverDi from "@/feature/common/server-di";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice";
|
import CustomerInvoice from "@/feature/core/customer-invoice/domain/entity/customer-invoice.entity";
|
||||||
import CustomerInvoiceRepo, {
|
import CustomerInvoiceRepo, {
|
||||||
customerInvoiceRepoKey,
|
customerInvoiceRepoKey,
|
||||||
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice-repo";
|
} from "@/feature/core/customer-invoice/domain/i-repo/customer-invoice.repo";
|
||||||
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice-module-key";
|
import { customerInvoiceModuleKey } from "@/feature/core/customer-invoice/invoice.module-key";
|
||||||
|
|
||||||
export default function fetchCustomerInvoicesUsecase(): Promise<
|
export default function fetchCustomerInvoicesUsecase(): Promise<
|
||||||
ApiEither<CustomerInvoice[]>
|
ApiEither<CustomerInvoice[]>
|
@ -1,7 +1,7 @@
|
|||||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||||
import ApiTask from "@/feature/common/data/api-task";
|
import ApiTask from "@/feature/common/data/api-task";
|
||||||
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
||||||
import NetworkFailure from "@/feature/common/failures/network-failure";
|
import NetworkFailure from "@/feature/common/failures/network.failure";
|
||||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||||
import CustomerRepo from "@/feature/core/customer/domain/i-repo/customer-repo";
|
import CustomerRepo from "@/feature/core/customer/domain/i-repo/customer-repo";
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import "server-only";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import serverDi from "@/feature/common/server-di";
|
|
||||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||||
import CustomerRepo, {
|
import CustomerRepo, {
|
||||||
customerRepoKey,
|
customerRepoKey,
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import "server-only";
|
|
||||||
import { ApiEither } from "@/feature/common/data/api-task";
|
import { ApiEither } from "@/feature/common/data/api-task";
|
||||||
import serverDi from "@/feature/common/server-di";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import { customerKey } from "@/feature/core/customer/customer-key";
|
import { customerKey } from "@/feature/core/customer/customer-key";
|
||||||
import Customer from "@/feature/core/customer/domain/entity/customer";
|
import Customer from "@/feature/core/customer/domain/entity/customer";
|
||||||
import CustomerRepo, {
|
import CustomerRepo, {
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
import di from "@/bootstrap/di/init-di";
|
import di from "@/bootstrap/di/init-di";
|
||||||
import invoiceDbRepo from "@/feature/core/invoice/data/repo/invoice-db-repo";
|
import invoiceDbRepo from "@/feature/core/invoice/data/repo/invoice-db.repo";
|
||||||
import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
import { invoiceRepoKey } from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||||
|
import createInvoiceUsecase from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice-impl.usecase";
|
||||||
|
import { createInvoiceUsecaseKey } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
|
||||||
import { DependencyContainer } from "tsyringe";
|
import { DependencyContainer } from "tsyringe";
|
||||||
|
|
||||||
export default function getInvoiceDi(): DependencyContainer {
|
export default function getInvoiceDi(): DependencyContainer {
|
||||||
const invoiceDi = di.createChildContainer();
|
const invoiceDi = di.createChildContainer();
|
||||||
|
|
||||||
invoiceDi.register(invoiceRepoKey, invoiceDbRepo);
|
invoiceDi.register(invoiceRepoKey, invoiceDbRepo);
|
||||||
|
invoiceDi.register(createInvoiceUsecaseKey, {
|
||||||
|
useValue: createInvoiceUsecase,
|
||||||
|
});
|
||||||
return invoiceDi;
|
return invoiceDi;
|
||||||
}
|
}
|
@ -1,11 +1,12 @@
|
|||||||
|
import "server-only";
|
||||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||||
import ApiTask from "@/feature/common/data/api-task";
|
import ApiTask from "@/feature/common/data/api-task";
|
||||||
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
import { failureOr } from "@/feature/common/failures/failure-helpers";
|
||||||
import NetworkFailure from "@/feature/common/failures/network-failure";
|
import NetworkFailure from "@/feature/common/failures/network.failure";
|
||||||
import { formatCurrency } from "@/feature/common/feature-helpers";
|
import { formatCurrency } from "@/feature/common/feature-helpers";
|
||||||
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
import InvoiceRepo from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||||
import { pipe } from "fp-ts/lib/function";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
import { tryCatch } from "fp-ts/lib/TaskEither";
|
import { tryCatch } from "fp-ts/lib/TaskEither";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
@ -1,6 +1,6 @@
|
|||||||
import ApiTask from "@/feature/common/data/api-task";
|
import ApiTask from "@/feature/common/data/api-task";
|
||||||
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice-param";
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||||
|
|
||||||
export default interface InvoiceRepo {
|
export default interface InvoiceRepo {
|
||||||
fetchAllInvoicesAmount(): Promise<number>;
|
fetchAllInvoicesAmount(): Promise<number>;
|
@ -1,22 +1,21 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
import { ApiEither } from "@/feature/common/data/api-task";
|
import { ApiEither } from "@/feature/common/data/api-task";
|
||||||
import ParamsFailure from "@/feature/common/failures/params-failure";
|
import ParamsFailure from "@/feature/common/failures/params.failure";
|
||||||
import serverDi from "@/feature/common/server-di";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import InvoiceRepo, {
|
import InvoiceRepo, {
|
||||||
invoiceRepoKey,
|
invoiceRepoKey,
|
||||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||||
import {
|
import {
|
||||||
InvoiceParam,
|
InvoiceParam,
|
||||||
invoiceSchema,
|
invoiceSchema,
|
||||||
} from "@/feature/core/invoice/domain/param/invoice-param";
|
} from "@/feature/core/invoice/domain/param/invoice.param";
|
||||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||||
|
import { CreateInvoiceUsecase } from "@/feature/core/invoice/domain/usecase/create-invoice/create-invoice.usecase";
|
||||||
import { pipe } from "fp-ts/lib/function";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
|
import { chain, fromNullable, left, map, right } from "fp-ts/lib/TaskEither";
|
||||||
|
|
||||||
export default async function createInvoiceUsecase(
|
const createInvoiceUsecase: CreateInvoiceUsecase = async (
|
||||||
params: InvoiceParam,
|
params: InvoiceParam,
|
||||||
): Promise<ApiEither<string>> {
|
): Promise<ApiEither<string>> => {
|
||||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
||||||
|
|
||||||
return pipe(
|
return pipe(
|
||||||
@ -29,4 +28,6 @@ export default async function createInvoiceUsecase(
|
|||||||
}),
|
}),
|
||||||
chain((params) => repo.createInvoice(params)),
|
chain((params) => repo.createInvoice(params)),
|
||||||
)();
|
)();
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export default createInvoiceUsecase;
|
@ -0,0 +1,8 @@
|
|||||||
|
import { ApiEither } from "@/feature/common/data/api-task";
|
||||||
|
import { InvoiceParam } from "@/feature/core/invoice/domain/param/invoice.param";
|
||||||
|
|
||||||
|
export type CreateInvoiceUsecase = (
|
||||||
|
param: InvoiceParam,
|
||||||
|
) => Promise<ApiEither<string>>;
|
||||||
|
|
||||||
|
export const createInvoiceUsecaseKey = "createInvoiceUsecaseKey";
|
@ -1,9 +1,8 @@
|
|||||||
import "server-only";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import serverDi from "@/feature/common/server-di";
|
|
||||||
import InvoiceRepo, {
|
import InvoiceRepo, {
|
||||||
invoiceRepoKey,
|
invoiceRepoKey,
|
||||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||||
|
|
||||||
export default function fetchAllInvoicesAmountUsecase(): Promise<number> {
|
export default function fetchAllInvoicesAmountUsecase(): Promise<number> {
|
||||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
@ -1,10 +1,9 @@
|
|||||||
import "server-only";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import serverDi from "@/feature/common/server-di";
|
|
||||||
import InvoiceRepo, {
|
import InvoiceRepo, {
|
||||||
invoiceRepoKey,
|
invoiceRepoKey,
|
||||||
} from "@/feature/core/invoice/domain/i-repo/invoice-repo";
|
} from "@/feature/core/invoice/domain/i-repo/invoice.i-repo";
|
||||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||||
import { invoiceModuleKey } from "@/feature/core/invoice/invoice-module-key";
|
import { invoiceModuleKey } from "@/feature/core/invoice/invoice.module-key";
|
||||||
|
|
||||||
export default function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
export default function fetchInvoicesStatusSummary(): Promise<InvoiceStatusSummary> {
|
||||||
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
const repo = serverDi(invoiceModuleKey).resolve<InvoiceRepo>(invoiceRepoKey);
|
@ -1,6 +1,6 @@
|
|||||||
import di from "@/bootstrap/di/init-di";
|
import di from "@/bootstrap/di/init-di";
|
||||||
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db-repo";
|
import RevenueDbRepo from "@/feature/core/revenue/data/repo/revenue-db.repo";
|
||||||
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
import { revenueRepoKey } from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
|
||||||
|
|
||||||
export default function getRevenueDi() {
|
export default function getRevenueDi() {
|
||||||
const revenueDi = di.createChildContainer();
|
const revenueDi = di.createChildContainer();
|
@ -1,6 +1,6 @@
|
|||||||
import { sql } from "@/bootstrap/boundaries/db/db";
|
import { sql } from "@/bootstrap/boundaries/db/db";
|
||||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||||
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
import RevenueRepo from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
|
|
||||||
export type RevenueDbResponse = {
|
export type RevenueDbResponse = {
|
@ -1,4 +1,4 @@
|
|||||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||||
|
|
||||||
export default interface RevenueRepo {
|
export default interface RevenueRepo {
|
||||||
fetchRevenues(): Promise<Revenue[]>;
|
fetchRevenues(): Promise<Revenue[]>;
|
@ -1,10 +1,9 @@
|
|||||||
import "server-only";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import serverDi from "@/feature/common/server-di";
|
import Revenue from "@/feature/core/revenue/domain/entity/revenue.entity";
|
||||||
import Revenue from "@/feature/core/revenue/domain/entity/revenue";
|
|
||||||
import RevenueRepo, {
|
import RevenueRepo, {
|
||||||
revenueRepoKey,
|
revenueRepoKey,
|
||||||
} from "@/feature/core/revenue/domain/i-repo/revenue-repo";
|
} from "@/feature/core/revenue/domain/i-repo/revenue.i-repo";
|
||||||
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue-module-key";
|
import { revenueModuleKey } from "@/feature/core/revenue/domain/revenue.module-key";
|
||||||
|
|
||||||
export default function fetchRevenuesUsecase(): Promise<Revenue[]> {
|
export default function fetchRevenuesUsecase(): Promise<Revenue[]> {
|
||||||
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey);
|
const repo = serverDi(revenueModuleKey).resolve<RevenueRepo>(revenueRepoKey);
|
@ -1,6 +1,6 @@
|
|||||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
||||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase";
|
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount.usecase";
|
||||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary";
|
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary.usecase";
|
||||||
import di from "@/bootstrap/di/init-di";
|
import di from "@/bootstrap/di/init-di";
|
||||||
|
|
||||||
export default function getSummaryInfoDi() {
|
export default function getSummaryInfoDi() {
|
@ -1,10 +1,9 @@
|
|||||||
import "server-only";
|
import serverDi from "@/feature/common/server.di";
|
||||||
import serverDi from "@/feature/common/server-di";
|
|
||||||
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
import fetchCustomersAmountUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-amount-usecase";
|
||||||
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount-usecase";
|
import fetchAllInvoicesAmountUsecase from "@/feature/core/invoice/domain/usecase/fetch-all-invoices-amount.usecase";
|
||||||
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary";
|
import fetchInvoicesStatusSummary from "@/feature/core/invoice/domain/usecase/fetch-invoices-status-summary.usecase";
|
||||||
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info-module-key";
|
import { summaryInfoModuleKey } from "@/feature/core/summary-info/domain/summary-info.module-key";
|
||||||
import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary-info";
|
import SummaryInfo from "@/feature/core/summary-info/domain/value-object/summary-info.value-object";
|
||||||
|
|
||||||
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
|
export default async function fetchSummaryInfoUsecase(): Promise<SummaryInfo> {
|
||||||
try {
|
try {
|
@ -1,4 +1,4 @@
|
|||||||
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status";
|
import InvoiceStatusSummary from "@/feature/core/invoice/domain/value-object/invoice-status.value-object";
|
||||||
|
|
||||||
export default class SummaryInfo {
|
export default class SummaryInfo {
|
||||||
customersNumber: number;
|
customersNumber: number;
|
@ -1,6 +1,9 @@
|
|||||||
import di from "@/bootstrap/di/init-di";
|
import di from "@/bootstrap/di/init-di";
|
||||||
import * as serverDi from "@/feature/common/server-di";
|
import * as serverDi from "@/feature/common/server.di";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To mock and get server di
|
||||||
|
*/
|
||||||
export default function mockDi() {
|
export default function mockDi() {
|
||||||
vi.spyOn(serverDi, "default").mockReturnValue(di);
|
vi.spyOn(serverDi, "default").mockReturnValue(di);
|
||||||
return di;
|
return di;
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { Mock } from "moq.ts";
|
import { Mock } from "moq.ts";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To get mock object to mock objects and classes
|
||||||
|
*/
|
||||||
export function getMock<T>() {
|
export function getMock<T>() {
|
||||||
return new Mock<T>();
|
return new Mock<T>();
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import CustomerRepo, {
|
|||||||
import { getMock } from "@/test/common/mock/mock-factory";
|
import { getMock } from "@/test/common/mock/mock-factory";
|
||||||
import { describe } from "vitest";
|
import { describe } from "vitest";
|
||||||
import { faker } from "@faker-js/faker";
|
import { faker } from "@faker-js/faker";
|
||||||
import CustomerFakeFactory from "@/test/common/fake-factory/customer/customer-fake-factory";
|
import CustomerFakeFactory from "@/test/common/fake-factory/customer/customer.fake-factory";
|
||||||
import fetchCustomersUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-usecase";
|
import fetchCustomersUsecase from "@/feature/core/customer/domain/usecase/fetch-customers-usecase";
|
||||||
import mockDi from "@/test/common/mock/mock-di";
|
import mockDi from "@/test/common/mock/mock-di";
|
||||||
import { right } from "fp-ts/lib/TaskEither";
|
import { right } from "fp-ts/lib/TaskEither";
|
Loading…
x
Reference in New Issue
Block a user