feature/research-di #1
10
package.json
10
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
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,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"types": ["vitest/globals"],
|
||||
"plugins": [
|
||||
{
|
||||
"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