feature/research-di #1

Merged
behnam merged 37 commits from feature/research-di into develop 2024-11-21 15:50:19 +00:00
17 changed files with 1291 additions and 64 deletions
Showing only changes of commit 7b2ec83068 - Show all commits

View File

@ -7,6 +7,7 @@
"build": "next build",
"start": "next start --port 4000",
"lint": "next lint",
"test": "vitest",
"seed": "node -r dotenv/config ./src/bootstrap/db/seed.js"
},
"dependencies": {
@ -26,15 +27,22 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@faker-js/faker": "^9.1.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.1",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@vitejs/plugin-react": "^4.3.3",
"bcrypt": "^5.1.1",
"dotenv": "^16.4.5",
"eslint": "^8",
"eslint-config-next": "15.0.1",
"jsdom": "^25.0.1",
"moq.ts": "^10.0.8",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"typescript": "^5",
"vitest": "^2.1.4"
}
}

View File

@ -1,10 +1,8 @@
import { sql } from '@/bootstrap/db/db';
import {
CustomerField,
CustomersTableType,
InvoiceForm,
InvoicesTable,
User,
Revenue,
Invoice,
Customer,
@ -189,25 +187,6 @@ export async function fetchInvoiceById(id: string) {
}
}
export async function fetchCustomers() {
// This is equivalent to in fetch(..., {cache: 'no-store'}).
connection()
try {
const data = await sql`
SELECT
id,
name
FROM customers
ORDER BY name ASC
` as postgres.RowList<CustomerField[]>;
return data;
} catch (err) {
console.error('Database Error:', err);
throw new Error('Failed to fetch all customers.');
}
}
export async function fetchFilteredCustomers(query: string) {
// This is equivalent to in fetch(..., {cache: 'no-store'}).
connection()
@ -242,13 +221,3 @@ export async function fetchFilteredCustomers(query: string) {
throw new Error('Failed to fetch customer table.');
}
}
export async function getUser(email: string) {
try {
const user = await sql`SELECT * FROM users WHERE email=${email}`;
return user.at(0) as User;
} catch (error) {
console.error('Failed to fetch user:', error);
throw new Error('Failed to fetch user.');
}
}

View File

@ -1,14 +1,3 @@
// This file contains type definitions for your data.
// It describes the shape of the data, and what data type each property should accept.
// For simplicity of teaching, we're manually defining these types.
// However, these types are generated automatically if you're using an ORM such as Prisma.
export type User = {
id: string;
name: string;
email: string;
password: string;
};
export type Customer = {
id: string;
name: string;
@ -73,11 +62,6 @@ export type FormattedCustomersTable = {
total_paid: string;
};
export type CustomerField = {
id: string;
name: string;
};
export type InvoiceForm = {
id: string;
customer_id: string;

View File

@ -1,10 +1,13 @@
import { customerKey } from "@/feature/customer/customer-key";
import getCustomerDi from "@/feature/customer/data/module/customer-di";
import { testModuleKey } from "@/feature/domain/test/test-module-key";
import getTestModule from "@/feature/infra/test/module/test-module";
import { DependencyContainer } from "tsyringe";
export default function serverDi(module: string): DependencyContainer {
const getDi = {
[testModuleKey]: getTestModule
[testModuleKey]: getTestModule,
[customerKey]: getCustomerDi
}[module]
if (!getDi) throw new Error("Server Di didn't found for module: " + module)

View File

@ -0,0 +1 @@
export const customerKey = "customerKey"

View File

@ -0,0 +1,13 @@
import di from "@/bootstrap/di/init-di";
import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase";
import { DependencyContainer } from "tsyringe";
export default function getCustomerDi(): DependencyContainer {
const customerDi = di.createChildContainer()
customerDi.register(fetchCustomersUsecase.name, {
useValue: fetchCustomersUsecase
})
return customerDi
}

View File

@ -0,0 +1,37 @@
export type CustomersTableType = {
id: string;
name: string;
email: string;
image_url: string;
total_invoices: number;
total_pending: number;
total_paid: number;
};
export default class Customer {
id: string;
name: string;
email: string;
imageUrl: string;
totalInvoices: number;
totalPending: number;
totalPaid: number;
constructor({
id,
email,
imageUrl,
name,
totalInvoices,
totalPaid,
totalPending
}: Customer) {
this.id = id;
this.name = name;
this.email = email;
this.imageUrl = imageUrl;
this.totalInvoices = totalInvoices;
this.totalPaid = totalPaid;
this.totalPending = totalPending;
}
}

View File

@ -0,0 +1,7 @@
import Customer from "@/feature/customer/domain/entity/customer"
export default interface CustomerRepo {
fetchList(query: string): Promise<Customer[]>
}
export const customerRepoKey = "customerRepoKey"

View File

@ -0,0 +1,12 @@
"use server"
import serverDi from "@/feature/common/server-di";
import { customerKey } from "@/feature/customer/customer-key";
import Customer from "@/feature/customer/domain/entity/customer";
import CustomerRepo, { customerRepoKey } from "@/feature/customer/domain/i-repo/customer-repo";
export default function fetchCustomersUsecase(query: string): Promise<Customer[]> {
const repo = serverDi(customerKey).resolve<CustomerRepo>(customerRepoKey)
return repo.fetchList(query)
}

View File

@ -0,0 +1,21 @@
import Customer from "@/feature/customer/domain/entity/customer";
import { faker } from "@faker-js/faker";
export default class CustomerFakeFactory {
static getFakeCustomer(): Customer {
return new Customer({
id: faker.string.uuid(),
name: faker.person.fullName(),
email: faker.internet.email(),
imageUrl: faker.image.url(),
totalInvoices: faker.number.int(),
totalPaid: faker.number.int(),
totalPending: faker.number.int(),
})
}
static getFakeCustomerList(length: number = 10): Customer[] {
return Array.from({length}).map(() => CustomerFakeFactory.getFakeCustomer())
}
}

View File

@ -0,0 +1,7 @@
import di from "@/bootstrap/di/init-di"
import * as serverDi from "@/feature/common/server-di"
export default function mockDi() {
vi.spyOn(serverDi, "default").mockReturnValue(di)
return di
}

View File

@ -0,0 +1,5 @@
import { Mock } from "moq.ts";
export function getMock<T>() {
return new Mock<T>();
}

1
src/test/setup.ts Normal file
View File

@ -0,0 +1 @@
import "reflect-metadata";

View File

@ -0,0 +1,48 @@
import CustomerRepo, { customerRepoKey } from "@/feature/customer/domain/i-repo/customer-repo";
import { getMock } from "@/test/common/mock/mock-factory";
import { describe } from "vitest";
import { faker } from "@faker-js/faker";
import CustomerFakeFactory from "@/test/common/fake-factory/customer/customer-fake-factory";
import fetchCustomersUsecase from "@/feature/customer/domain/usecase/fetch-customers-usecase";
import mockDi from "@/test/common/mock/mock-di";
/* -------------------------------------------------------------------------- */
/* Faking */
/* -------------------------------------------------------------------------- */
const fakedCustomerList = CustomerFakeFactory.getFakeCustomerList()
/* -------------------------------------------------------------------------- */
/* Mocking */
/* -------------------------------------------------------------------------- */
const customerDi = mockDi()
const mockedFetchList = vi.fn<CustomerRepo['fetchList']>()
const MockedRepo = getMock<CustomerRepo>()
MockedRepo.setup((instance) => instance.fetchList).returns(mockedFetchList)
/* -------------------------------------------------------------------------- */
/* DI */
/* -------------------------------------------------------------------------- */
customerDi.register(fetchCustomersUsecase.name, {
useValue: fetchCustomersUsecase
})
customerDi.register(customerRepoKey, {
useValue: MockedRepo.object()
})
/* -------------------------------------------------------------------------- */
/* Testing */
/* -------------------------------------------------------------------------- */
const usecase = customerDi.resolve<typeof fetchCustomersUsecase>(fetchCustomersUsecase.name)
describe("Fetch customers", () => {
describe("On given query string", () => {
const fakedQuery = faker.person.fullName();
describe("And returning list from repo", () => {
beforeEach(() => {
mockedFetchList.mockResolvedValue(fakedCustomerList)
})
it("Then should return correct list of customers", async () => {
// ! Act
const response = await usecase(fakedQuery)
// ? Assert
expect(response).toEqual(fakedCustomerList)
})
});
});
});

View File

@ -15,6 +15,7 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"types": ["vitest/globals"],
"plugins": [
{
"name": "next"

15
vite.config.ts Normal file
View File

@ -0,0 +1,15 @@
/// <reference types="vitest" />
import { defineConfig } from "vitest/config";
import path from 'path';
export default defineConfig({
test: {
globals: true,
setupFiles: 'src/test/setup.ts',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})

1125
yarn.lock

File diff suppressed because it is too large Load Diff