feature/research-di #1
10
package.json
10
package.json
@ -7,6 +7,7 @@
|
|||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start --port 4000",
|
"start": "next start --port 4000",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
|
"test": "vitest",
|
||||||
"seed": "node -r dotenv/config ./src/bootstrap/db/seed.js"
|
"seed": "node -r dotenv/config ./src/bootstrap/db/seed.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -26,15 +27,22 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@faker-js/faker": "^9.1.0",
|
||||||
|
"@testing-library/dom": "^10.4.0",
|
||||||
|
"@testing-library/react": "^16.0.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "15.0.1",
|
"eslint-config-next": "15.0.1",
|
||||||
|
"jsdom": "^25.0.1",
|
||||||
|
"moq.ts": "^10.0.8",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5",
|
||||||
|
"vitest": "^2.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { sql } from '@/bootstrap/db/db';
|
import { sql } from '@/bootstrap/db/db';
|
||||||
import {
|
import {
|
||||||
CustomerField,
|
|
||||||
CustomersTableType,
|
CustomersTableType,
|
||||||
InvoiceForm,
|
InvoiceForm,
|
||||||
InvoicesTable,
|
InvoicesTable,
|
||||||
User,
|
|
||||||
Revenue,
|
Revenue,
|
||||||
Invoice,
|
Invoice,
|
||||||
Customer,
|
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) {
|
export async function fetchFilteredCustomers(query: string) {
|
||||||
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
// This is equivalent to in fetch(..., {cache: 'no-store'}).
|
||||||
connection()
|
connection()
|
||||||
@ -242,13 +221,3 @@ export async function fetchFilteredCustomers(query: string) {
|
|||||||
throw new Error('Failed to fetch customer table.');
|
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.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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 = {
|
export type Customer = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -73,11 +62,6 @@ export type FormattedCustomersTable = {
|
|||||||
total_paid: string;
|
total_paid: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomerField = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type InvoiceForm = {
|
export type InvoiceForm = {
|
||||||
id: string;
|
id: string;
|
||||||
customer_id: string;
|
customer_id: string;
|
||||||
|
@ -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 { testModuleKey } from "@/feature/domain/test/test-module-key";
|
||||||
import getTestModule from "@/feature/infra/test/module/test-module";
|
import getTestModule from "@/feature/infra/test/module/test-module";
|
||||||
import { DependencyContainer } from "tsyringe";
|
import { DependencyContainer } from "tsyringe";
|
||||||
|
|
||||||
export default function serverDi(module: string): DependencyContainer {
|
export default function serverDi(module: string): DependencyContainer {
|
||||||
const getDi = {
|
const getDi = {
|
||||||
[testModuleKey]: getTestModule
|
[testModuleKey]: getTestModule,
|
||||||
|
[customerKey]: getCustomerDi
|
||||||
}[module]
|
}[module]
|
||||||
|
|
||||||
if (!getDi) throw new Error("Server Di didn't found for module: " + module)
|
if (!getDi) throw new Error("Server Di didn't found for module: " + module)
|
||||||
|
1
src/feature/customer/customer-key.ts
Normal file
1
src/feature/customer/customer-key.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const customerKey = "customerKey"
|
13
src/feature/customer/data/module/customer-di.ts
Normal file
13
src/feature/customer/data/module/customer-di.ts
Normal 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
|
||||||
|
}
|
37
src/feature/customer/domain/entity/customer.ts
Normal file
37
src/feature/customer/domain/entity/customer.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
7
src/feature/customer/domain/i-repo/customer-repo.ts
Normal file
7
src/feature/customer/domain/i-repo/customer-repo.ts
Normal 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"
|
@ -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)
|
||||||
|
}
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
7
src/test/common/mock/mock-di.ts
Normal file
7
src/test/common/mock/mock-di.ts
Normal 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
|
||||||
|
}
|
5
src/test/common/mock/mock-factory.ts
Normal file
5
src/test/common/mock/mock-factory.ts
Normal 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
1
src/test/setup.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
import "reflect-metadata";
|
@ -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)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -15,6 +15,7 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
"types": ["vitest/globals"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
|
15
vite.config.ts
Normal file
15
vite.config.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user