mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
style: format all files with prettier
This commit is contained in:
@@ -7,6 +7,7 @@ model: opus
|
||||
You are a TypeScript expert specializing in advanced typing and enterprise-grade development.
|
||||
|
||||
## Focus Areas
|
||||
|
||||
- Advanced type systems (generics, conditional types, mapped types)
|
||||
- Strict TypeScript configuration and compiler options
|
||||
- Type inference optimization and utility types
|
||||
@@ -15,6 +16,7 @@ You are a TypeScript expert specializing in advanced typing and enterprise-grade
|
||||
- Integration with modern frameworks (React, Node.js, Express)
|
||||
|
||||
## Approach
|
||||
|
||||
1. Leverage strict type checking with appropriate compiler flags
|
||||
2. Use generics and utility types for maximum type safety
|
||||
3. Prefer type inference over explicit annotations when clear
|
||||
@@ -23,6 +25,7 @@ You are a TypeScript expert specializing in advanced typing and enterprise-grade
|
||||
6. Optimize build times with incremental compilation
|
||||
|
||||
## Output
|
||||
|
||||
- Strongly-typed TypeScript with comprehensive interfaces
|
||||
- Generic functions and classes with proper constraints
|
||||
- Custom utility types and advanced type manipulations
|
||||
|
||||
@@ -15,6 +15,7 @@ $ARGUMENTS
|
||||
### 1. Analyze Project Type
|
||||
|
||||
Determine the project type from user requirements:
|
||||
|
||||
- **Next.js**: Full-stack React applications, SSR/SSG, API routes
|
||||
- **React + Vite**: SPA applications, component libraries
|
||||
- **Node.js API**: Express/Fastify backends, microservices
|
||||
@@ -82,6 +83,7 @@ nextjs-project/
|
||||
```
|
||||
|
||||
**package.json**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "nextjs-project",
|
||||
@@ -112,6 +114,7 @@ nextjs-project/
|
||||
```
|
||||
|
||||
**tsconfig.json**:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
@@ -131,7 +134,7 @@ nextjs-project/
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"plugins": [{"name": "next"}]
|
||||
"plugins": [{ "name": "next" }]
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
@@ -146,16 +149,17 @@ pnpm create vite . --template react-ts
|
||||
```
|
||||
|
||||
**vite.config.ts**:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
@@ -163,10 +167,10 @@ export default defineConfig({
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
setupFiles: './tests/setup.ts',
|
||||
environment: "jsdom",
|
||||
setupFiles: "./tests/setup.ts",
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### 5. Generate Node.js API Project Structure
|
||||
@@ -202,6 +206,7 @@ nodejs-api/
|
||||
```
|
||||
|
||||
**package.json for Node.js API**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "nodejs-api",
|
||||
@@ -233,21 +238,22 @@ nodejs-api/
|
||||
```
|
||||
|
||||
**src/app.ts**:
|
||||
|
||||
```typescript
|
||||
import express, { Express } from 'express'
|
||||
import { healthRouter } from './routes/health.js'
|
||||
import { userRouter } from './routes/users.js'
|
||||
import { errorHandler } from './middleware/errorHandler.js'
|
||||
import express, { Express } from "express";
|
||||
import { healthRouter } from "./routes/health.js";
|
||||
import { userRouter } from "./routes/users.js";
|
||||
import { errorHandler } from "./middleware/errorHandler.js";
|
||||
|
||||
export function createApp(): Express {
|
||||
const app = express()
|
||||
const app = express();
|
||||
|
||||
app.use(express.json())
|
||||
app.use('/health', healthRouter)
|
||||
app.use('/api/users', userRouter)
|
||||
app.use(errorHandler)
|
||||
app.use(express.json());
|
||||
app.use("/health", healthRouter);
|
||||
app.use("/api/users", userRouter);
|
||||
app.use(errorHandler);
|
||||
|
||||
return app
|
||||
return app;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -267,6 +273,7 @@ library-name/
|
||||
```
|
||||
|
||||
**package.json for Library**:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@scope/library-name",
|
||||
@@ -296,6 +303,7 @@ library-name/
|
||||
### 7. Configure Development Tools
|
||||
|
||||
**.env.example**:
|
||||
|
||||
```env
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
@@ -304,29 +312,28 @@ JWT_SECRET=your-secret-key
|
||||
```
|
||||
|
||||
**vitest.config.ts**:
|
||||
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
environment: "node",
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
**.eslintrc.json**:
|
||||
|
||||
```json
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "error"
|
||||
|
||||
@@ -23,19 +23,20 @@ Comprehensive guide for implementing robust testing strategies in JavaScript/Typ
|
||||
### Jest - Full-Featured Testing Framework
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// jest.config.ts
|
||||
import type { Config } from 'jest';
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/*.interface.ts',
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/**/*.interface.ts",
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
@@ -45,7 +46,7 @@ const config: Config = {
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
|
||||
setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -54,20 +55,21 @@ export default config;
|
||||
### Vitest - Fast, Vite-Native Testing
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
environment: "node",
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'html'],
|
||||
exclude: ['**/*.d.ts', '**/*.config.ts', '**/dist/**'],
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["**/*.d.ts", "**/*.config.ts", "**/dist/**"],
|
||||
},
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
},
|
||||
});
|
||||
```
|
||||
@@ -84,42 +86,42 @@ export function add(a: number, b: number): number {
|
||||
|
||||
export function divide(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
throw new Error('Division by zero');
|
||||
throw new Error("Division by zero");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
|
||||
// utils/calculator.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { add, divide } from './calculator';
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { add, divide } from "./calculator";
|
||||
|
||||
describe('Calculator', () => {
|
||||
describe('add', () => {
|
||||
it('should add two positive numbers', () => {
|
||||
describe("Calculator", () => {
|
||||
describe("add", () => {
|
||||
it("should add two positive numbers", () => {
|
||||
expect(add(2, 3)).toBe(5);
|
||||
});
|
||||
|
||||
it('should add negative numbers', () => {
|
||||
it("should add negative numbers", () => {
|
||||
expect(add(-2, -3)).toBe(-5);
|
||||
});
|
||||
|
||||
it('should handle zero', () => {
|
||||
it("should handle zero", () => {
|
||||
expect(add(0, 5)).toBe(5);
|
||||
expect(add(5, 0)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('divide', () => {
|
||||
it('should divide two numbers', () => {
|
||||
describe("divide", () => {
|
||||
it("should divide two numbers", () => {
|
||||
expect(divide(10, 2)).toBe(5);
|
||||
});
|
||||
|
||||
it('should handle decimal results', () => {
|
||||
it("should handle decimal results", () => {
|
||||
expect(divide(5, 2)).toBe(2.5);
|
||||
});
|
||||
|
||||
it('should throw error when dividing by zero', () => {
|
||||
expect(() => divide(10, 0)).toThrow('Division by zero');
|
||||
it("should throw error when dividing by zero", () => {
|
||||
expect(() => divide(10, 0)).toThrow("Division by zero");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -134,7 +136,7 @@ export class UserService {
|
||||
|
||||
create(user: User): User {
|
||||
if (this.users.has(user.id)) {
|
||||
throw new Error('User already exists');
|
||||
throw new Error("User already exists");
|
||||
}
|
||||
this.users.set(user.id, user);
|
||||
return user;
|
||||
@@ -147,7 +149,7 @@ export class UserService {
|
||||
update(id: string, updates: Partial<User>): User {
|
||||
const user = this.users.get(id);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const updated = { ...user, ...updates };
|
||||
this.users.set(id, updated);
|
||||
@@ -160,47 +162,48 @@ export class UserService {
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { UserService } from './user.service';
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { UserService } from "./user.service";
|
||||
|
||||
describe('UserService', () => {
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new UserService();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('should create a new user', () => {
|
||||
const user = { id: '1', name: 'John', email: 'john@example.com' };
|
||||
describe("create", () => {
|
||||
it("should create a new user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
const created = service.create(user);
|
||||
|
||||
expect(created).toEqual(user);
|
||||
expect(service.findById('1')).toEqual(user);
|
||||
expect(service.findById("1")).toEqual(user);
|
||||
});
|
||||
|
||||
it('should throw error if user already exists', () => {
|
||||
const user = { id: '1', name: 'John', email: 'john@example.com' };
|
||||
it("should throw error if user already exists", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
expect(() => service.create(user)).toThrow('User already exists');
|
||||
expect(() => service.create(user)).toThrow("User already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('should update existing user', () => {
|
||||
const user = { id: '1', name: 'John', email: 'john@example.com' };
|
||||
describe("update", () => {
|
||||
it("should update existing user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
const updated = service.update('1', { name: 'Jane' });
|
||||
const updated = service.update("1", { name: "Jane" });
|
||||
|
||||
expect(updated.name).toBe('Jane');
|
||||
expect(updated.email).toBe('john@example.com');
|
||||
expect(updated.name).toBe("Jane");
|
||||
expect(updated.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it('should throw error if user not found', () => {
|
||||
expect(() => service.update('999', { name: 'Jane' }))
|
||||
.toThrow('User not found');
|
||||
it("should throw error if user not found", () => {
|
||||
expect(() => service.update("999", { name: "Jane" })).toThrow(
|
||||
"User not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -214,15 +217,15 @@ export class ApiService {
|
||||
async fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`https://api.example.com/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('User not found');
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createUser(user: CreateUserDTO): Promise<User> {
|
||||
const response = await fetch('https://api.example.com/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
const response = await fetch("https://api.example.com/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
return response.json();
|
||||
@@ -230,13 +233,13 @@ export class ApiService {
|
||||
}
|
||||
|
||||
// services/api.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { ApiService } from './api.service';
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ApiService } from "./api.service";
|
||||
|
||||
// Mock fetch globally
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe('ApiService', () => {
|
||||
describe("ApiService", () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -244,34 +247,34 @@ describe('ApiService', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('fetchUser', () => {
|
||||
it('should fetch user successfully', async () => {
|
||||
const mockUser = { id: '1', name: 'John', email: 'john@example.com' };
|
||||
describe("fetchUser", () => {
|
||||
it("should fetch user successfully", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockUser,
|
||||
});
|
||||
|
||||
const user = await service.fetchUser('1');
|
||||
const user = await service.fetchUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(fetch).toHaveBeenCalledWith('https://api.example.com/users/1');
|
||||
expect(fetch).toHaveBeenCalledWith("https://api.example.com/users/1");
|
||||
});
|
||||
|
||||
it('should throw error if user not found', async () => {
|
||||
it("should throw error if user not found", async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
});
|
||||
|
||||
await expect(service.fetchUser('999')).rejects.toThrow('User not found');
|
||||
await expect(service.fetchUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUser', () => {
|
||||
it('should create user successfully', async () => {
|
||||
const newUser = { name: 'John', email: 'john@example.com' };
|
||||
const createdUser = { id: '1', ...newUser };
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const newUser = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...newUser };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
@@ -282,11 +285,11 @@ describe('ApiService', () => {
|
||||
|
||||
expect(user).toEqual(createdUser);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
'https://api.example.com/users',
|
||||
"https://api.example.com/users",
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
method: "POST",
|
||||
body: JSON.stringify(newUser),
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -299,7 +302,7 @@ describe('ApiService', () => {
|
||||
|
||||
```typescript
|
||||
// services/email.service.ts
|
||||
import nodemailer from 'nodemailer';
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export class EmailService {
|
||||
private transporter = nodemailer.createTransport({
|
||||
@@ -322,36 +325,36 @@ export class EmailService {
|
||||
}
|
||||
|
||||
// services/email.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { EmailService } from './email.service';
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EmailService } from "./email.service";
|
||||
|
||||
vi.mock('nodemailer', () => ({
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: {
|
||||
createTransport: vi.fn(() => ({
|
||||
sendMail: vi.fn().mockResolvedValue({ messageId: '123' }),
|
||||
sendMail: vi.fn().mockResolvedValue({ messageId: "123" }),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('EmailService', () => {
|
||||
describe("EmailService", () => {
|
||||
let service: EmailService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new EmailService();
|
||||
});
|
||||
|
||||
it('should send email successfully', async () => {
|
||||
it("should send email successfully", async () => {
|
||||
await service.sendEmail(
|
||||
'test@example.com',
|
||||
'Test Subject',
|
||||
'<p>Test Body</p>'
|
||||
"test@example.com",
|
||||
"Test Subject",
|
||||
"<p>Test Body</p>",
|
||||
);
|
||||
|
||||
expect(service['transporter'].sendMail).toHaveBeenCalledWith(
|
||||
expect(service["transporter"].sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: 'test@example.com',
|
||||
subject: 'Test Subject',
|
||||
})
|
||||
to: "test@example.com",
|
||||
subject: "Test Subject",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -372,7 +375,7 @@ export class UserService {
|
||||
async getUser(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
@@ -385,10 +388,10 @@ export class UserService {
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { UserService, IUserRepository } from './user.service';
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { UserService, IUserRepository } from "./user.service";
|
||||
|
||||
describe('UserService', () => {
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
let mockRepository: IUserRepository;
|
||||
|
||||
@@ -400,28 +403,28 @@ describe('UserService', () => {
|
||||
service = new UserService(mockRepository);
|
||||
});
|
||||
|
||||
describe('getUser', () => {
|
||||
it('should return user if found', async () => {
|
||||
const mockUser = { id: '1', name: 'John', email: 'john@example.com' };
|
||||
describe("getUser", () => {
|
||||
it("should return user if found", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(mockUser);
|
||||
|
||||
const user = await service.getUser('1');
|
||||
const user = await service.getUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith('1');
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith("1");
|
||||
});
|
||||
|
||||
it('should throw error if user not found', async () => {
|
||||
it("should throw error if user not found", async () => {
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(null);
|
||||
|
||||
await expect(service.getUser('999')).rejects.toThrow('User not found');
|
||||
await expect(service.getUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe('createUser', () => {
|
||||
it('should create user successfully', async () => {
|
||||
const userData = { name: 'John', email: 'john@example.com' };
|
||||
const createdUser = { id: '1', ...userData };
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const userData = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...userData };
|
||||
|
||||
vi.mocked(mockRepository.create).mockResolvedValue(createdUser);
|
||||
|
||||
@@ -444,7 +447,7 @@ export const logger = {
|
||||
};
|
||||
|
||||
// services/order.service.ts
|
||||
import { logger } from '../utils/logger';
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class OrderService {
|
||||
async processOrder(orderId: string): Promise<void> {
|
||||
@@ -455,28 +458,28 @@ export class OrderService {
|
||||
}
|
||||
|
||||
// services/order.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { OrderService } from './order.service';
|
||||
import { logger } from '../utils/logger';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { OrderService } from "./order.service";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
describe('OrderService', () => {
|
||||
describe("OrderService", () => {
|
||||
let service: OrderService;
|
||||
let loggerSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new OrderService();
|
||||
loggerSpy = vi.spyOn(logger, 'info');
|
||||
loggerSpy = vi.spyOn(logger, "info");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should log order processing', async () => {
|
||||
await service.processOrder('123');
|
||||
it("should log order processing", async () => {
|
||||
await service.processOrder("123");
|
||||
|
||||
expect(loggerSpy).toHaveBeenCalledWith('Processing order 123');
|
||||
expect(loggerSpy).toHaveBeenCalledWith('Order 123 processed successfully');
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Processing order 123");
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Order 123 processed successfully");
|
||||
expect(loggerSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@@ -488,37 +491,37 @@ describe('OrderService', () => {
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.api.test.ts
|
||||
import request from 'supertest';
|
||||
import { app } from '../../src/app';
|
||||
import { pool } from '../../src/config/database';
|
||||
import request from "supertest";
|
||||
import { app } from "../../src/app";
|
||||
import { pool } from "../../src/config/database";
|
||||
|
||||
describe('User API Integration Tests', () => {
|
||||
describe("User API Integration Tests", () => {
|
||||
beforeAll(async () => {
|
||||
// Setup test database
|
||||
await pool.query('CREATE TABLE IF NOT EXISTS users (...)');
|
||||
await pool.query("CREATE TABLE IF NOT EXISTS users (...)");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await pool.query('DROP TABLE IF EXISTS users');
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear data before each test
|
||||
await pool.query('TRUNCATE TABLE users CASCADE');
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
describe('POST /api/users', () => {
|
||||
it('should create a new user', async () => {
|
||||
describe("POST /api/users", () => {
|
||||
it("should create a new user", async () => {
|
||||
const userData = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/users')
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
@@ -526,52 +529,50 @@ describe('User API Integration Tests', () => {
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
});
|
||||
expect(response.body).toHaveProperty('id');
|
||||
expect(response.body).not.toHaveProperty('password');
|
||||
expect(response.body).toHaveProperty("id");
|
||||
expect(response.body).not.toHaveProperty("password");
|
||||
});
|
||||
|
||||
it('should return 400 if email is invalid', async () => {
|
||||
it("should return 400 if email is invalid", async () => {
|
||||
const userData = {
|
||||
name: 'John Doe',
|
||||
email: 'invalid-email',
|
||||
password: 'password123',
|
||||
name: "John Doe",
|
||||
email: "invalid-email",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/users')
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body).toHaveProperty("error");
|
||||
});
|
||||
|
||||
it('should return 409 if email already exists', async () => {
|
||||
it("should return 409 if email already exists", async () => {
|
||||
const userData = {
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
await request(app).post('/api/users').send(userData);
|
||||
await request(app).post("/api/users").send(userData);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/api/users')
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(409);
|
||||
|
||||
expect(response.body.error).toContain('already exists');
|
||||
expect(response.body.error).toContain("already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/users/:id', () => {
|
||||
it('should get user by id', async () => {
|
||||
const createResponse = await request(app)
|
||||
.post('/api/users')
|
||||
.send({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
describe("GET /api/users/:id", () => {
|
||||
it("should get user by id", async () => {
|
||||
const createResponse = await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const userId = createResponse.body.id;
|
||||
|
||||
@@ -581,50 +582,42 @@ describe('User API Integration Tests', () => {
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: userId,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 404 if user not found', async () => {
|
||||
await request(app)
|
||||
.get('/api/users/999')
|
||||
.expect(404);
|
||||
it("should return 404 if user not found", async () => {
|
||||
await request(app).get("/api/users/999").expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Authentication', () => {
|
||||
it('should require authentication for protected routes', async () => {
|
||||
await request(app)
|
||||
.get('/api/users/me')
|
||||
.expect(401);
|
||||
describe("Authentication", () => {
|
||||
it("should require authentication for protected routes", async () => {
|
||||
await request(app).get("/api/users/me").expect(401);
|
||||
});
|
||||
|
||||
it('should allow access with valid token', async () => {
|
||||
it("should allow access with valid token", async () => {
|
||||
// Create user and login
|
||||
await request(app)
|
||||
.post('/api/users')
|
||||
.send({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const loginResponse = await request(app)
|
||||
.post('/api/auth/login')
|
||||
.send({
|
||||
email: 'john@example.com',
|
||||
password: 'password123',
|
||||
});
|
||||
const loginResponse = await request(app).post("/api/auth/login").send({
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const token = loginResponse.body.token;
|
||||
|
||||
const response = await request(app)
|
||||
.get('/api/users/me')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.get("/api/users/me")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.email).toBe('john@example.com');
|
||||
expect(response.body.email).toBe("john@example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -634,21 +627,21 @@ describe('User API Integration Tests', () => {
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.repository.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
|
||||
import { Pool } from 'pg';
|
||||
import { UserRepository } from '../../src/repositories/user.repository';
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "../../src/repositories/user.repository";
|
||||
|
||||
describe('UserRepository Integration Tests', () => {
|
||||
describe("UserRepository Integration Tests", () => {
|
||||
let pool: Pool;
|
||||
let repository: UserRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
pool = new Pool({
|
||||
host: 'localhost',
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: 'test_db',
|
||||
user: 'test_user',
|
||||
password: 'test_password',
|
||||
database: "test_db",
|
||||
user: "test_user",
|
||||
password: "test_password",
|
||||
});
|
||||
|
||||
repository = new UserRepository(pool);
|
||||
@@ -666,41 +659,41 @@ describe('UserRepository Integration Tests', () => {
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await pool.query('DROP TABLE IF EXISTS users');
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await pool.query('TRUNCATE TABLE users CASCADE');
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
it('should create a user', async () => {
|
||||
it("should create a user", async () => {
|
||||
const user = await repository.create({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password',
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
expect(user).toHaveProperty('id');
|
||||
expect(user.name).toBe('John Doe');
|
||||
expect(user.email).toBe('john@example.com');
|
||||
expect(user).toHaveProperty("id");
|
||||
expect(user.name).toBe("John Doe");
|
||||
expect(user.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it('should find user by email', async () => {
|
||||
it("should find user by email", async () => {
|
||||
await repository.create({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
password: 'hashed_password',
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
const user = await repository.findByEmail('john@example.com');
|
||||
const user = await repository.findByEmail("john@example.com");
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user?.name).toBe('John Doe');
|
||||
expect(user?.name).toBe("John Doe");
|
||||
});
|
||||
|
||||
it('should return null if user not found', async () => {
|
||||
const user = await repository.findByEmail('nonexistent@example.com');
|
||||
it("should return null if user not found", async () => {
|
||||
const user = await repository.findByEmail("nonexistent@example.com");
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -799,7 +792,7 @@ describe('UserForm', () => {
|
||||
|
||||
```typescript
|
||||
// hooks/useCounter.ts
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
export function useCounter(initialValue = 0) {
|
||||
const [count, setCount] = useState(initialValue);
|
||||
@@ -812,22 +805,22 @@ export function useCounter(initialValue = 0) {
|
||||
}
|
||||
|
||||
// hooks/useCounter.test.ts
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { useCounter } from './useCounter';
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { useCounter } from "./useCounter";
|
||||
|
||||
describe('useCounter', () => {
|
||||
it('should initialize with default value', () => {
|
||||
describe("useCounter", () => {
|
||||
it("should initialize with default value", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
expect(result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it('should initialize with custom value', () => {
|
||||
it("should initialize with custom value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
|
||||
it('should increment count', () => {
|
||||
it("should increment count", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
|
||||
act(() => {
|
||||
@@ -837,7 +830,7 @@ describe('useCounter', () => {
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
|
||||
it('should decrement count', () => {
|
||||
it("should decrement count", () => {
|
||||
const { result } = renderHook(() => useCounter(5));
|
||||
|
||||
act(() => {
|
||||
@@ -847,7 +840,7 @@ describe('useCounter', () => {
|
||||
expect(result.current.count).toBe(4);
|
||||
});
|
||||
|
||||
it('should reset to initial value', () => {
|
||||
it("should reset to initial value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
|
||||
act(() => {
|
||||
@@ -870,7 +863,7 @@ describe('useCounter', () => {
|
||||
|
||||
```typescript
|
||||
// tests/fixtures/user.fixture.ts
|
||||
import { faker } from '@faker-js/faker';
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export function createUserFixture(overrides?: Partial<User>): User {
|
||||
return {
|
||||
@@ -887,15 +880,18 @@ export function createUsersFixture(count: number): User[] {
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
import { createUserFixture, createUsersFixture } from '../fixtures/user.fixture';
|
||||
import {
|
||||
createUserFixture,
|
||||
createUsersFixture,
|
||||
} from "../fixtures/user.fixture";
|
||||
|
||||
describe('UserService', () => {
|
||||
it('should process user', () => {
|
||||
const user = createUserFixture({ name: 'John Doe' });
|
||||
describe("UserService", () => {
|
||||
it("should process user", () => {
|
||||
const user = createUserFixture({ name: "John Doe" });
|
||||
// Use user in test
|
||||
});
|
||||
|
||||
it('should handle multiple users', () => {
|
||||
it("should handle multiple users", () => {
|
||||
const users = createUsersFixture(10);
|
||||
// Use users in test
|
||||
});
|
||||
@@ -967,16 +963,16 @@ describe('UserCard', () => {
|
||||
### Test Organization
|
||||
|
||||
```typescript
|
||||
describe('UserService', () => {
|
||||
describe('createUser', () => {
|
||||
it('should create user successfully', () => {});
|
||||
it('should throw error if email exists', () => {});
|
||||
it('should hash password', () => {});
|
||||
describe("UserService", () => {
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", () => {});
|
||||
it("should throw error if email exists", () => {});
|
||||
it("should hash password", () => {});
|
||||
});
|
||||
|
||||
describe('updateUser', () => {
|
||||
it('should update user', () => {});
|
||||
it('should throw error if not found', () => {});
|
||||
describe("updateUser", () => {
|
||||
it("should update user", () => {});
|
||||
it("should throw error if not found", () => {});
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -985,23 +981,23 @@ describe('UserService', () => {
|
||||
|
||||
```typescript
|
||||
// Using async/await
|
||||
it('should fetch user', async () => {
|
||||
const user = await service.fetchUser('1');
|
||||
it("should fetch user", async () => {
|
||||
const user = await service.fetchUser("1");
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
// Testing rejections
|
||||
it('should throw error', async () => {
|
||||
await expect(service.fetchUser('invalid')).rejects.toThrow('Not found');
|
||||
it("should throw error", async () => {
|
||||
await expect(service.fetchUser("invalid")).rejects.toThrow("Not found");
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Timers
|
||||
|
||||
```typescript
|
||||
import { vi } from 'vitest';
|
||||
import { vi } from "vitest";
|
||||
|
||||
it('should call function after delay', () => {
|
||||
it("should call function after delay", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
|
||||
@@ -23,6 +23,7 @@ Comprehensive guide for mastering modern JavaScript (ES6+) features, functional
|
||||
### 1. Arrow Functions
|
||||
|
||||
**Syntax and Use Cases:**
|
||||
|
||||
```javascript
|
||||
// Traditional function
|
||||
function add(a, b) {
|
||||
@@ -33,13 +34,13 @@ function add(a, b) {
|
||||
const add = (a, b) => a + b;
|
||||
|
||||
// Single parameter (parentheses optional)
|
||||
const double = x => x * 2;
|
||||
const double = (x) => x * 2;
|
||||
|
||||
// No parameters
|
||||
const getRandom = () => Math.random();
|
||||
|
||||
// Multiple statements (need curly braces)
|
||||
const processUser = user => {
|
||||
const processUser = (user) => {
|
||||
const normalized = user.name.toLowerCase();
|
||||
return { ...user, name: normalized };
|
||||
};
|
||||
@@ -49,6 +50,7 @@ const createUser = (name, age) => ({ name, age });
|
||||
```
|
||||
|
||||
**Lexical 'this' Binding:**
|
||||
|
||||
```javascript
|
||||
class Counter {
|
||||
constructor() {
|
||||
@@ -62,15 +64,15 @@ class Counter {
|
||||
|
||||
// Traditional function loses 'this' in callbacks
|
||||
incrementTraditional() {
|
||||
setTimeout(function() {
|
||||
this.count++; // 'this' is undefined
|
||||
setTimeout(function () {
|
||||
this.count++; // 'this' is undefined
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Arrow function maintains 'this'
|
||||
incrementArrow() {
|
||||
setTimeout(() => {
|
||||
this.count++; // 'this' refers to Counter instance
|
||||
this.count++; // 'this' refers to Counter instance
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
@@ -79,15 +81,16 @@ class Counter {
|
||||
### 2. Destructuring
|
||||
|
||||
**Object Destructuring:**
|
||||
|
||||
```javascript
|
||||
const user = {
|
||||
id: 1,
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
address: {
|
||||
city: 'New York',
|
||||
country: 'USA'
|
||||
}
|
||||
city: "New York",
|
||||
country: "USA",
|
||||
},
|
||||
};
|
||||
|
||||
// Basic destructuring
|
||||
@@ -100,7 +103,9 @@ const { name: userName, email: userEmail } = user;
|
||||
const { age = 25 } = user;
|
||||
|
||||
// Nested destructuring
|
||||
const { address: { city, country } } = user;
|
||||
const {
|
||||
address: { city, country },
|
||||
} = user;
|
||||
|
||||
// Rest operator
|
||||
const { id, ...userWithoutId } = user;
|
||||
@@ -113,6 +118,7 @@ greet(user);
|
||||
```
|
||||
|
||||
**Array Destructuring:**
|
||||
|
||||
```javascript
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
|
||||
@@ -126,7 +132,8 @@ const [, , third] = numbers;
|
||||
const [head, ...tail] = numbers;
|
||||
|
||||
// Swapping variables
|
||||
let a = 1, b = 2;
|
||||
let a = 1,
|
||||
b = 2;
|
||||
[a, b] = [b, a];
|
||||
|
||||
// Function return values
|
||||
@@ -142,6 +149,7 @@ const [one, two, three = 0] = [1, 2];
|
||||
### 3. Spread and Rest Operators
|
||||
|
||||
**Spread Operator:**
|
||||
|
||||
```javascript
|
||||
// Array spreading
|
||||
const arr1 = [1, 2, 3];
|
||||
@@ -149,8 +157,8 @@ const arr2 = [4, 5, 6];
|
||||
const combined = [...arr1, ...arr2];
|
||||
|
||||
// Object spreading
|
||||
const defaults = { theme: 'dark', lang: 'en' };
|
||||
const userPrefs = { theme: 'light' };
|
||||
const defaults = { theme: "dark", lang: "en" };
|
||||
const userPrefs = { theme: "light" };
|
||||
const settings = { ...defaults, ...userPrefs };
|
||||
|
||||
// Function arguments
|
||||
@@ -167,6 +175,7 @@ const newObj = { ...user, age: 30 };
|
||||
```
|
||||
|
||||
**Rest Parameters:**
|
||||
|
||||
```javascript
|
||||
// Collect function arguments
|
||||
function sum(...numbers) {
|
||||
@@ -176,9 +185,9 @@ sum(1, 2, 3, 4, 5);
|
||||
|
||||
// With regular parameters
|
||||
function greet(greeting, ...names) {
|
||||
return `${greeting} ${names.join(', ')}`;
|
||||
return `${greeting} ${names.join(", ")}`;
|
||||
}
|
||||
greet('Hello', 'John', 'Jane', 'Bob');
|
||||
greet("Hello", "John", "Jane", "Bob");
|
||||
|
||||
// Object rest
|
||||
const { id, ...userData } = user;
|
||||
@@ -191,7 +200,7 @@ const [first, ...rest] = [1, 2, 3, 4, 5];
|
||||
|
||||
```javascript
|
||||
// Basic usage
|
||||
const name = 'John';
|
||||
const name = "John";
|
||||
const greeting = `Hello, ${name}!`;
|
||||
|
||||
// Multi-line strings
|
||||
@@ -209,12 +218,12 @@ const total = `Total: $${(price * 1.2).toFixed(2)}`;
|
||||
// Tagged template literals
|
||||
function highlight(strings, ...values) {
|
||||
return strings.reduce((result, str, i) => {
|
||||
const value = values[i] || '';
|
||||
const value = values[i] || "";
|
||||
return result + str + `<mark>${value}</mark>`;
|
||||
}, '');
|
||||
}, "");
|
||||
}
|
||||
|
||||
const name = 'John';
|
||||
const name = "John";
|
||||
const age = 30;
|
||||
const html = highlight`Name: ${name}, Age: ${age}`;
|
||||
// Output: "Name: <mark>John</mark>, Age: <mark>30</mark>"
|
||||
@@ -223,7 +232,7 @@ const html = highlight`Name: ${name}, Age: ${age}`;
|
||||
### 5. Enhanced Object Literals
|
||||
|
||||
```javascript
|
||||
const name = 'John';
|
||||
const name = "John";
|
||||
const age = 30;
|
||||
|
||||
// Shorthand property names
|
||||
@@ -236,28 +245,31 @@ const calculator = {
|
||||
},
|
||||
subtract(a, b) {
|
||||
return a - b;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Computed property names
|
||||
const field = 'email';
|
||||
const field = "email";
|
||||
const user = {
|
||||
name: 'John',
|
||||
[field]: 'john@example.com',
|
||||
name: "John",
|
||||
[field]: "john@example.com",
|
||||
[`get${field.charAt(0).toUpperCase()}${field.slice(1)}`]() {
|
||||
return this[field];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Dynamic property creation
|
||||
const createUser = (name, ...props) => {
|
||||
return props.reduce((user, [key, value]) => ({
|
||||
...user,
|
||||
[key]: value
|
||||
}), { name });
|
||||
return props.reduce(
|
||||
(user, [key, value]) => ({
|
||||
...user,
|
||||
[key]: value,
|
||||
}),
|
||||
{ name },
|
||||
);
|
||||
};
|
||||
|
||||
const user = createUser('John', ['age', 30], ['email', 'john@example.com']);
|
||||
const user = createUser("John", ["age", 30], ["email", "john@example.com"]);
|
||||
```
|
||||
|
||||
## Asynchronous Patterns
|
||||
@@ -265,15 +277,16 @@ const user = createUser('John', ['age', 30], ['email', 'john@example.com']);
|
||||
### 1. Promises
|
||||
|
||||
**Creating and Using Promises:**
|
||||
|
||||
```javascript
|
||||
// Creating a promise
|
||||
const fetchUser = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (id > 0) {
|
||||
resolve({ id, name: 'John' });
|
||||
resolve({ id, name: "John" });
|
||||
} else {
|
||||
reject(new Error('Invalid ID'));
|
||||
reject(new Error("Invalid ID"));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
@@ -281,57 +294,54 @@ const fetchUser = (id) => {
|
||||
|
||||
// Using promises
|
||||
fetchUser(1)
|
||||
.then(user => console.log(user))
|
||||
.catch(error => console.error(error))
|
||||
.finally(() => console.log('Done'));
|
||||
.then((user) => console.log(user))
|
||||
.catch((error) => console.error(error))
|
||||
.finally(() => console.log("Done"));
|
||||
|
||||
// Chaining promises
|
||||
fetchUser(1)
|
||||
.then(user => fetchUserPosts(user.id))
|
||||
.then(posts => processPosts(posts))
|
||||
.then(result => console.log(result))
|
||||
.catch(error => console.error(error));
|
||||
.then((user) => fetchUserPosts(user.id))
|
||||
.then((posts) => processPosts(posts))
|
||||
.then((result) => console.log(result))
|
||||
.catch((error) => console.error(error));
|
||||
```
|
||||
|
||||
**Promise Combinators:**
|
||||
|
||||
```javascript
|
||||
// Promise.all - Wait for all promises
|
||||
const promises = [
|
||||
fetchUser(1),
|
||||
fetchUser(2),
|
||||
fetchUser(3)
|
||||
];
|
||||
const promises = [fetchUser(1), fetchUser(2), fetchUser(3)];
|
||||
|
||||
Promise.all(promises)
|
||||
.then(users => console.log(users))
|
||||
.catch(error => console.error('At least one failed:', error));
|
||||
.then((users) => console.log(users))
|
||||
.catch((error) => console.error("At least one failed:", error));
|
||||
|
||||
// Promise.allSettled - Wait for all, regardless of outcome
|
||||
Promise.allSettled(promises)
|
||||
.then(results => {
|
||||
results.forEach(result => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log('Success:', result.value);
|
||||
} else {
|
||||
console.log('Error:', result.reason);
|
||||
}
|
||||
});
|
||||
Promise.allSettled(promises).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === "fulfilled") {
|
||||
console.log("Success:", result.value);
|
||||
} else {
|
||||
console.log("Error:", result.reason);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Promise.race - First to complete
|
||||
Promise.race(promises)
|
||||
.then(winner => console.log('First:', winner))
|
||||
.catch(error => console.error(error));
|
||||
.then((winner) => console.log("First:", winner))
|
||||
.catch((error) => console.error(error));
|
||||
|
||||
// Promise.any - First to succeed
|
||||
Promise.any(promises)
|
||||
.then(first => console.log('First success:', first))
|
||||
.catch(error => console.error('All failed:', error));
|
||||
.then((first) => console.log("First success:", first))
|
||||
.catch((error) => console.error("All failed:", error));
|
||||
```
|
||||
|
||||
### 2. Async/Await
|
||||
|
||||
**Basic Usage:**
|
||||
|
||||
```javascript
|
||||
// Async function always returns a Promise
|
||||
async function fetchUser(id) {
|
||||
@@ -347,28 +357,26 @@ async function getUserData(id) {
|
||||
const posts = await fetchUserPosts(user.id);
|
||||
return { user, posts };
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
console.error("Error fetching data:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential vs Parallel execution
|
||||
async function sequential() {
|
||||
const user1 = await fetchUser(1); // Wait
|
||||
const user2 = await fetchUser(2); // Then wait
|
||||
const user1 = await fetchUser(1); // Wait
|
||||
const user2 = await fetchUser(2); // Then wait
|
||||
return [user1, user2];
|
||||
}
|
||||
|
||||
async function parallel() {
|
||||
const [user1, user2] = await Promise.all([
|
||||
fetchUser(1),
|
||||
fetchUser(2)
|
||||
]);
|
||||
const [user1, user2] = await Promise.all([fetchUser(1), fetchUser(2)]);
|
||||
return [user1, user2];
|
||||
}
|
||||
```
|
||||
|
||||
**Advanced Patterns:**
|
||||
|
||||
```javascript
|
||||
// Async IIFE
|
||||
(async () => {
|
||||
@@ -385,7 +393,7 @@ async function processUsers(userIds) {
|
||||
}
|
||||
|
||||
// Top-level await (ES2022)
|
||||
const config = await fetch('/config.json').then(r => r.json());
|
||||
const config = await fetch("/config.json").then((r) => r.json());
|
||||
|
||||
// Retry logic
|
||||
async function fetchWithRetry(url, retries = 3) {
|
||||
@@ -394,7 +402,7 @@ async function fetchWithRetry(url, retries = 3) {
|
||||
return await fetch(url);
|
||||
} catch (error) {
|
||||
if (i === retries - 1) throw error;
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,7 +410,7 @@ async function fetchWithRetry(url, retries = 3) {
|
||||
// Timeout wrapper
|
||||
async function withTimeout(promise, ms) {
|
||||
const timeout = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), ms)
|
||||
setTimeout(() => reject(new Error("Timeout")), ms),
|
||||
);
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
@@ -413,20 +421,21 @@ async function withTimeout(promise, ms) {
|
||||
### 1. Array Methods
|
||||
|
||||
**Map, Filter, Reduce:**
|
||||
|
||||
```javascript
|
||||
const users = [
|
||||
{ id: 1, name: 'John', age: 30, active: true },
|
||||
{ id: 2, name: 'Jane', age: 25, active: false },
|
||||
{ id: 3, name: 'Bob', age: 35, active: true }
|
||||
{ id: 1, name: "John", age: 30, active: true },
|
||||
{ id: 2, name: "Jane", age: 25, active: false },
|
||||
{ id: 3, name: "Bob", age: 35, active: true },
|
||||
];
|
||||
|
||||
// Map - Transform array
|
||||
const names = users.map(user => user.name);
|
||||
const upperNames = users.map(user => user.name.toUpperCase());
|
||||
const names = users.map((user) => user.name);
|
||||
const upperNames = users.map((user) => user.name.toUpperCase());
|
||||
|
||||
// Filter - Select elements
|
||||
const activeUsers = users.filter(user => user.active);
|
||||
const adults = users.filter(user => user.age >= 18);
|
||||
const activeUsers = users.filter((user) => user.active);
|
||||
const adults = users.filter((user) => user.age >= 18);
|
||||
|
||||
// Reduce - Aggregate data
|
||||
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
|
||||
@@ -434,44 +443,45 @@ const avgAge = totalAge / users.length;
|
||||
|
||||
// Group by property
|
||||
const byActive = users.reduce((groups, user) => {
|
||||
const key = user.active ? 'active' : 'inactive';
|
||||
const key = user.active ? "active" : "inactive";
|
||||
return {
|
||||
...groups,
|
||||
[key]: [...(groups[key] || []), user]
|
||||
[key]: [...(groups[key] || []), user],
|
||||
};
|
||||
}, {});
|
||||
|
||||
// Chaining methods
|
||||
const result = users
|
||||
.filter(user => user.active)
|
||||
.map(user => user.name)
|
||||
.filter((user) => user.active)
|
||||
.map((user) => user.name)
|
||||
.sort()
|
||||
.join(', ');
|
||||
.join(", ");
|
||||
```
|
||||
|
||||
**Advanced Array Methods:**
|
||||
|
||||
```javascript
|
||||
// Find - First matching element
|
||||
const user = users.find(u => u.id === 2);
|
||||
const user = users.find((u) => u.id === 2);
|
||||
|
||||
// FindIndex - Index of first match
|
||||
const index = users.findIndex(u => u.name === 'Jane');
|
||||
const index = users.findIndex((u) => u.name === "Jane");
|
||||
|
||||
// Some - At least one matches
|
||||
const hasActive = users.some(u => u.active);
|
||||
const hasActive = users.some((u) => u.active);
|
||||
|
||||
// Every - All match
|
||||
const allAdults = users.every(u => u.age >= 18);
|
||||
const allAdults = users.every((u) => u.age >= 18);
|
||||
|
||||
// FlatMap - Map and flatten
|
||||
const userTags = [
|
||||
{ name: 'John', tags: ['admin', 'user'] },
|
||||
{ name: 'Jane', tags: ['user'] }
|
||||
{ name: "John", tags: ["admin", "user"] },
|
||||
{ name: "Jane", tags: ["user"] },
|
||||
];
|
||||
const allTags = userTags.flatMap(u => u.tags);
|
||||
const allTags = userTags.flatMap((u) => u.tags);
|
||||
|
||||
// From - Create array from iterable
|
||||
const str = 'hello';
|
||||
const str = "hello";
|
||||
const chars = Array.from(str);
|
||||
const numbers = Array.from({ length: 5 }, (_, i) => i + 1);
|
||||
|
||||
@@ -482,6 +492,7 @@ const arr = Array.of(1, 2, 3);
|
||||
### 2. Higher-Order Functions
|
||||
|
||||
**Functions as Arguments:**
|
||||
|
||||
```javascript
|
||||
// Custom forEach
|
||||
function forEach(array, callback) {
|
||||
@@ -512,14 +523,15 @@ function filter(array, predicate) {
|
||||
```
|
||||
|
||||
**Functions Returning Functions:**
|
||||
|
||||
```javascript
|
||||
// Currying
|
||||
const multiply = a => b => a * b;
|
||||
const multiply = (a) => (b) => a * b;
|
||||
const double = multiply(2);
|
||||
const triple = multiply(3);
|
||||
|
||||
console.log(double(5)); // 10
|
||||
console.log(triple(5)); // 15
|
||||
console.log(double(5)); // 10
|
||||
console.log(triple(5)); // 15
|
||||
|
||||
// Partial application
|
||||
function partial(fn, ...args) {
|
||||
@@ -528,7 +540,7 @@ function partial(fn, ...args) {
|
||||
|
||||
const add = (a, b, c) => a + b + c;
|
||||
const add5 = partial(add, 5);
|
||||
console.log(add5(3, 2)); // 10
|
||||
console.log(add5(3, 2)); // 10
|
||||
|
||||
// Memoization
|
||||
function memoize(fn) {
|
||||
@@ -554,34 +566,38 @@ const fibonacci = memoize((n) => {
|
||||
|
||||
```javascript
|
||||
// Function composition
|
||||
const compose = (...fns) => x =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), x);
|
||||
const compose =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), x);
|
||||
|
||||
const pipe = (...fns) => x =>
|
||||
fns.reduce((acc, fn) => fn(acc), x);
|
||||
const pipe =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduce((acc, fn) => fn(acc), x);
|
||||
|
||||
// Example usage
|
||||
const addOne = x => x + 1;
|
||||
const double = x => x * 2;
|
||||
const square = x => x * x;
|
||||
const addOne = (x) => x + 1;
|
||||
const double = (x) => x * 2;
|
||||
const square = (x) => x * x;
|
||||
|
||||
const composed = compose(square, double, addOne);
|
||||
console.log(composed(3)); // ((3 + 1) * 2)^2 = 64
|
||||
console.log(composed(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
const piped = pipe(addOne, double, square);
|
||||
console.log(piped(3)); // ((3 + 1) * 2)^2 = 64
|
||||
console.log(piped(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
// Practical example
|
||||
const processUser = pipe(
|
||||
user => ({ ...user, name: user.name.trim() }),
|
||||
user => ({ ...user, email: user.email.toLowerCase() }),
|
||||
user => ({ ...user, age: parseInt(user.age) })
|
||||
(user) => ({ ...user, name: user.name.trim() }),
|
||||
(user) => ({ ...user, email: user.email.toLowerCase() }),
|
||||
(user) => ({ ...user, age: parseInt(user.age) }),
|
||||
);
|
||||
|
||||
const user = processUser({
|
||||
name: ' John ',
|
||||
email: 'JOHN@EXAMPLE.COM',
|
||||
age: '30'
|
||||
name: " John ",
|
||||
email: "JOHN@EXAMPLE.COM",
|
||||
age: "30",
|
||||
});
|
||||
```
|
||||
|
||||
@@ -600,7 +616,7 @@ function addItemPure(cart, item) {
|
||||
return {
|
||||
...cart,
|
||||
items: [...cart.items, item],
|
||||
total: cart.total + item.price
|
||||
total: cart.total + item.price,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -611,28 +627,28 @@ const numbers = [1, 2, 3, 4, 5];
|
||||
const withSix = [...numbers, 6];
|
||||
|
||||
// Remove from array
|
||||
const withoutThree = numbers.filter(n => n !== 3);
|
||||
const withoutThree = numbers.filter((n) => n !== 3);
|
||||
|
||||
// Update array element
|
||||
const doubled = numbers.map(n => n === 3 ? n * 2 : n);
|
||||
const doubled = numbers.map((n) => (n === 3 ? n * 2 : n));
|
||||
|
||||
// Immutable object operations
|
||||
const user = { name: 'John', age: 30 };
|
||||
const user = { name: "John", age: 30 };
|
||||
|
||||
// Update property
|
||||
const olderUser = { ...user, age: 31 };
|
||||
|
||||
// Add property
|
||||
const withEmail = { ...user, email: 'john@example.com' };
|
||||
const withEmail = { ...user, email: "john@example.com" };
|
||||
|
||||
// Remove property
|
||||
const { age, ...withoutAge } = user;
|
||||
|
||||
// Deep cloning (simple approach)
|
||||
const deepClone = obj => JSON.parse(JSON.stringify(obj));
|
||||
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
// Better deep cloning
|
||||
const structuredClone = obj => globalThis.structuredClone(obj);
|
||||
const structuredClone = (obj) => globalThis.structuredClone(obj);
|
||||
```
|
||||
|
||||
## Modern Class Features
|
||||
@@ -716,21 +732,21 @@ export default function multiply(a, b) {
|
||||
|
||||
// Importing
|
||||
// app.js
|
||||
import multiply, { PI, add, Calculator } from './math.js';
|
||||
import multiply, { PI, add, Calculator } from "./math.js";
|
||||
|
||||
// Rename imports
|
||||
import { add as sum } from './math.js';
|
||||
import { add as sum } from "./math.js";
|
||||
|
||||
// Import all
|
||||
import * as Math from './math.js';
|
||||
import * as Math from "./math.js";
|
||||
|
||||
// Dynamic imports
|
||||
const module = await import('./math.js');
|
||||
const { add } = await import('./math.js');
|
||||
const module = await import("./math.js");
|
||||
const { add } = await import("./math.js");
|
||||
|
||||
// Conditional loading
|
||||
if (condition) {
|
||||
const module = await import('./feature.js');
|
||||
const module = await import("./feature.js");
|
||||
module.init();
|
||||
}
|
||||
```
|
||||
@@ -754,13 +770,13 @@ const range = {
|
||||
} else {
|
||||
return { done: true };
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
for (const num of range) {
|
||||
console.log(num); // 1, 2, 3, 4, 5
|
||||
console.log(num); // 1, 2, 3, 4, 5
|
||||
}
|
||||
|
||||
// Generator function
|
||||
@@ -795,7 +811,7 @@ async function* fetchPages(url) {
|
||||
}
|
||||
}
|
||||
|
||||
for await (const page of fetchPages('/api/users')) {
|
||||
for await (const page of fetchPages("/api/users")) {
|
||||
console.log(page);
|
||||
}
|
||||
```
|
||||
@@ -804,9 +820,9 @@ for await (const page of fetchPages('/api/users')) {
|
||||
|
||||
```javascript
|
||||
// Optional chaining
|
||||
const user = { name: 'John', address: { city: 'NYC' } };
|
||||
const user = { name: "John", address: { city: "NYC" } };
|
||||
const city = user?.address?.city;
|
||||
const zipCode = user?.address?.zipCode; // undefined
|
||||
const zipCode = user?.address?.zipCode; // undefined
|
||||
|
||||
// Function call
|
||||
const result = obj.method?.();
|
||||
@@ -815,21 +831,21 @@ const result = obj.method?.();
|
||||
const first = arr?.[0];
|
||||
|
||||
// Nullish coalescing
|
||||
const value = null ?? 'default'; // 'default'
|
||||
const value = undefined ?? 'default'; // 'default'
|
||||
const value = 0 ?? 'default'; // 0 (not 'default')
|
||||
const value = '' ?? 'default'; // '' (not 'default')
|
||||
const value = null ?? "default"; // 'default'
|
||||
const value = undefined ?? "default"; // 'default'
|
||||
const value = 0 ?? "default"; // 0 (not 'default')
|
||||
const value = "" ?? "default"; // '' (not 'default')
|
||||
|
||||
// Logical assignment
|
||||
let a = null;
|
||||
a ??= 'default'; // a = 'default'
|
||||
a ??= "default"; // a = 'default'
|
||||
|
||||
let b = 5;
|
||||
b ??= 10; // b = 5 (unchanged)
|
||||
b ??= 10; // b = 5 (unchanged)
|
||||
|
||||
let obj = { count: 0 };
|
||||
obj.count ||= 1; // obj.count = 1
|
||||
obj.count &&= 2; // obj.count = 2
|
||||
obj.count ||= 1; // obj.count = 1
|
||||
obj.count &&= 2; // obj.count = 2
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
@@ -853,7 +869,7 @@ function throttle(fn, limit) {
|
||||
if (!inThrottle) {
|
||||
fn(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -869,8 +885,8 @@ function* lazyMap(iterable, transform) {
|
||||
|
||||
// Use only what you need
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
const doubled = lazyMap(numbers, x => x * 2);
|
||||
const first = doubled.next().value; // Only computes first value
|
||||
const doubled = lazyMap(numbers, (x) => x * 2);
|
||||
const first = doubled.next().value; // Only computes first value
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -23,22 +23,23 @@ Comprehensive guidance for building scalable, maintainable, and production-ready
|
||||
### Express.js - Minimalist Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import express, { Request, Response, NextFunction } from 'express';
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import compression from 'compression';
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import compression from "compression";
|
||||
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
|
||||
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
|
||||
app.use(compression());
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -55,20 +56,21 @@ app.listen(PORT, () => {
|
||||
### Fastify - High Performance Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import Fastify from 'fastify';
|
||||
import helmet from '@fastify/helmet';
|
||||
import cors from '@fastify/cors';
|
||||
import compress from '@fastify/compress';
|
||||
import Fastify from "fastify";
|
||||
import helmet from "@fastify/helmet";
|
||||
import cors from "@fastify/cors";
|
||||
import compress from "@fastify/compress";
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
options: { colorize: true }
|
||||
}
|
||||
}
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Plugins
|
||||
@@ -80,23 +82,27 @@ await fastify.register(compress);
|
||||
fastify.post<{
|
||||
Body: { name: string; email: string };
|
||||
Reply: { id: string; name: string };
|
||||
}>('/users', {
|
||||
schema: {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['name', 'email'],
|
||||
properties: {
|
||||
name: { type: 'string', minLength: 1 },
|
||||
email: { type: 'string', format: 'email' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}, async (request, reply) => {
|
||||
const { name, email } = request.body;
|
||||
return { id: '123', name };
|
||||
});
|
||||
}>(
|
||||
"/users",
|
||||
{
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["name", "email"],
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1 },
|
||||
email: { type: "string", format: "email" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { name, email } = request.body;
|
||||
return { id: "123", name };
|
||||
},
|
||||
);
|
||||
|
||||
await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
||||
await fastify.listen({ port: 3000, host: "0.0.0.0" });
|
||||
```
|
||||
|
||||
## Architectural Patterns
|
||||
@@ -104,6 +110,7 @@ await fastify.listen({ port: 3000, host: '0.0.0.0' });
|
||||
### Pattern 1: Layered Architecture
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
src/
|
||||
├── controllers/ # Handle HTTP requests/responses
|
||||
@@ -118,11 +125,12 @@ src/
|
||||
```
|
||||
|
||||
**Controller Layer:**
|
||||
|
||||
```typescript
|
||||
// controllers/user.controller.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { UserService } from '../services/user.service';
|
||||
import { CreateUserDTO, UpdateUserDTO } from '../types/user.types';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { UserService } from "../services/user.service";
|
||||
import { CreateUserDTO, UpdateUserDTO } from "../types/user.types";
|
||||
|
||||
export class UserController {
|
||||
constructor(private userService: UserService) {}
|
||||
@@ -171,12 +179,13 @@ export class UserController {
|
||||
```
|
||||
|
||||
**Service Layer:**
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { CreateUserDTO, UpdateUserDTO, User } from '../types/user.types';
|
||||
import { NotFoundError, ValidationError } from '../utils/errors';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types";
|
||||
import { NotFoundError, ValidationError } from "../utils/errors";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export class UserService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
@@ -185,7 +194,7 @@ export class UserService {
|
||||
// Validation
|
||||
const existingUser = await this.userRepository.findByEmail(userData.email);
|
||||
if (existingUser) {
|
||||
throw new ValidationError('Email already exists');
|
||||
throw new ValidationError("Email already exists");
|
||||
}
|
||||
|
||||
// Hash password
|
||||
@@ -194,7 +203,7 @@ export class UserService {
|
||||
// Create user
|
||||
const user = await this.userRepository.create({
|
||||
...userData,
|
||||
password: hashedPassword
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
@@ -205,7 +214,7 @@ export class UserService {
|
||||
async getUserById(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundError('User not found');
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
@@ -214,7 +223,7 @@ export class UserService {
|
||||
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
|
||||
const user = await this.userRepository.update(id, updates);
|
||||
if (!user) {
|
||||
throw new NotFoundError('User not found');
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
@@ -223,22 +232,25 @@ export class UserService {
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
const deleted = await this.userRepository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new NotFoundError('User not found');
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Repository Layer:**
|
||||
|
||||
```typescript
|
||||
// repositories/user.repository.ts
|
||||
import { Pool } from 'pg';
|
||||
import { CreateUserDTO, UpdateUserDTO, UserEntity } from '../types/user.types';
|
||||
import { Pool } from "pg";
|
||||
import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types";
|
||||
|
||||
export class UserRepository {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async create(userData: CreateUserDTO & { password: string }): Promise<UserEntity> {
|
||||
async create(
|
||||
userData: CreateUserDTO & { password: string },
|
||||
): Promise<UserEntity> {
|
||||
const query = `
|
||||
INSERT INTO users (name, email, password)
|
||||
VALUES ($1, $2, $3)
|
||||
@@ -247,19 +259,19 @@ export class UserRepository {
|
||||
const { rows } = await this.db.query(query, [
|
||||
userData.name,
|
||||
userData.email,
|
||||
userData.password
|
||||
userData.password,
|
||||
]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<UserEntity | null> {
|
||||
const query = 'SELECT * FROM users WHERE id = $1';
|
||||
const query = "SELECT * FROM users WHERE id = $1";
|
||||
const { rows } = await this.db.query(query, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<UserEntity | null> {
|
||||
const query = 'SELECT * FROM users WHERE email = $1';
|
||||
const query = "SELECT * FROM users WHERE email = $1";
|
||||
const { rows } = await this.db.query(query, [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
@@ -270,7 +282,7 @@ export class UserRepository {
|
||||
|
||||
const setClause = fields
|
||||
.map((field, idx) => `${field} = $${idx + 2}`)
|
||||
.join(', ');
|
||||
.join(", ");
|
||||
|
||||
const query = `
|
||||
UPDATE users
|
||||
@@ -284,7 +296,7 @@ export class UserRepository {
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const query = 'DELETE FROM users WHERE id = $1';
|
||||
const query = "DELETE FROM users WHERE id = $1";
|
||||
const { rowCount } = await this.db.query(query, [id]);
|
||||
return rowCount > 0;
|
||||
}
|
||||
@@ -294,13 +306,14 @@ export class UserRepository {
|
||||
### Pattern 2: Dependency Injection
|
||||
|
||||
**DI Container:**
|
||||
|
||||
```typescript
|
||||
// di-container.ts
|
||||
import { Pool } from 'pg';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { UserService } from './services/user.service';
|
||||
import { UserController } from './controllers/user.controller';
|
||||
import { AuthService } from './services/auth.service';
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "./repositories/user.repository";
|
||||
import { UserService } from "./services/user.service";
|
||||
import { UserController } from "./controllers/user.controller";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
|
||||
class Container {
|
||||
private instances = new Map<string, any>();
|
||||
@@ -331,31 +344,39 @@ class Container {
|
||||
export const container = new Container();
|
||||
|
||||
// Register dependencies
|
||||
container.singleton('db', () => new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}));
|
||||
|
||||
container.singleton('userRepository', () =>
|
||||
new UserRepository(container.resolve('db'))
|
||||
container.singleton(
|
||||
"db",
|
||||
() =>
|
||||
new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}),
|
||||
);
|
||||
|
||||
container.singleton('userService', () =>
|
||||
new UserService(container.resolve('userRepository'))
|
||||
container.singleton(
|
||||
"userRepository",
|
||||
() => new UserRepository(container.resolve("db")),
|
||||
);
|
||||
|
||||
container.register('userController', () =>
|
||||
new UserController(container.resolve('userService'))
|
||||
container.singleton(
|
||||
"userService",
|
||||
() => new UserService(container.resolve("userRepository")),
|
||||
);
|
||||
|
||||
container.singleton('authService', () =>
|
||||
new AuthService(container.resolve('userRepository'))
|
||||
container.register(
|
||||
"userController",
|
||||
() => new UserController(container.resolve("userService")),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"authService",
|
||||
() => new AuthService(container.resolve("userRepository")),
|
||||
);
|
||||
```
|
||||
|
||||
@@ -365,9 +386,9 @@ container.singleton('authService', () =>
|
||||
|
||||
```typescript
|
||||
// middleware/auth.middleware.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { UnauthorizedError } from '../utils/errors';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
@@ -385,40 +406,35 @@ declare global {
|
||||
export const authenticate = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedError('No token provided');
|
||||
throw new UnauthorizedError("No token provided");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(
|
||||
token,
|
||||
process.env.JWT_SECRET!
|
||||
) as JWTPayload;
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
||||
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(new UnauthorizedError('Invalid token'));
|
||||
next(new UnauthorizedError("Invalid token"));
|
||||
}
|
||||
};
|
||||
|
||||
export const authorize = (...roles: string[]) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return next(new UnauthorizedError('Not authenticated'));
|
||||
return next(new UnauthorizedError("Not authenticated"));
|
||||
}
|
||||
|
||||
// Check if user has required role
|
||||
const hasRole = roles.some(role =>
|
||||
req.user?.roles?.includes(role)
|
||||
);
|
||||
const hasRole = roles.some((role) => req.user?.roles?.includes(role));
|
||||
|
||||
if (!hasRole) {
|
||||
return next(new UnauthorizedError('Insufficient permissions'));
|
||||
return next(new UnauthorizedError("Insufficient permissions"));
|
||||
}
|
||||
|
||||
next();
|
||||
@@ -430,9 +446,9 @@ export const authorize = (...roles: string[]) => {
|
||||
|
||||
```typescript
|
||||
// middleware/validation.middleware.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { AnyZodObject, ZodError } from 'zod';
|
||||
import { ValidationError } from '../utils/errors';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AnyZodObject, ZodError } from "zod";
|
||||
import { ValidationError } from "../utils/errors";
|
||||
|
||||
export const validate = (schema: AnyZodObject) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
@@ -440,16 +456,16 @@ export const validate = (schema: AnyZodObject) => {
|
||||
await schema.parseAsync({
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
params: req.params
|
||||
params: req.params,
|
||||
});
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const errors = error.errors.map(err => ({
|
||||
field: err.path.join('.'),
|
||||
message: err.message
|
||||
const errors = error.errors.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
next(new ValidationError('Validation failed', errors));
|
||||
next(new ValidationError("Validation failed", errors));
|
||||
} else {
|
||||
next(error);
|
||||
}
|
||||
@@ -458,40 +474,40 @@ export const validate = (schema: AnyZodObject) => {
|
||||
};
|
||||
|
||||
// Usage with Zod
|
||||
import { z } from 'zod';
|
||||
import { z } from "zod";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
password: z.string().min(8),
|
||||
}),
|
||||
});
|
||||
|
||||
router.post('/users', validate(createUserSchema), userController.createUser);
|
||||
router.post("/users", validate(createUserSchema), userController.createUser);
|
||||
```
|
||||
|
||||
### Rate Limiting Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/rate-limit.middleware.ts
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import RedisStore from 'rate-limit-redis';
|
||||
import Redis from 'ioredis';
|
||||
import rateLimit from "express-rate-limit";
|
||||
import RedisStore from "rate-limit-redis";
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || '6379')
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
});
|
||||
|
||||
export const apiLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: 'rl:',
|
||||
prefix: "rl:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per windowMs
|
||||
message: 'Too many requests from this IP, please try again later',
|
||||
message: "Too many requests from this IP, please try again later",
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
@@ -499,7 +515,7 @@ export const apiLimiter = rateLimit({
|
||||
export const authLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: 'rl:auth:',
|
||||
prefix: "rl:auth:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5, // Stricter limit for auth endpoints
|
||||
@@ -511,34 +527,34 @@ export const authLimiter = rateLimit({
|
||||
|
||||
```typescript
|
||||
// middleware/logger.middleware.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import pino from 'pino';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import pino from "pino";
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
options: { colorize: true }
|
||||
}
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
});
|
||||
|
||||
export const requestLogger = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const start = Date.now();
|
||||
|
||||
// Log response when finished
|
||||
res.on('finish', () => {
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
logger.info({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
duration: `${duration}ms`,
|
||||
userAgent: req.headers['user-agent'],
|
||||
ip: req.ip
|
||||
userAgent: req.headers["user-agent"],
|
||||
ip: req.ip,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -558,7 +574,7 @@ export class AppError extends Error {
|
||||
constructor(
|
||||
public message: string,
|
||||
public statusCode: number = 500,
|
||||
public isOperational: boolean = true
|
||||
public isOperational: boolean = true,
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
@@ -567,25 +583,28 @@ export class AppError extends Error {
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message: string, public errors?: any[]) {
|
||||
constructor(
|
||||
message: string,
|
||||
public errors?: any[],
|
||||
) {
|
||||
super(message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message: string = 'Resource not found') {
|
||||
constructor(message: string = "Resource not found") {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends AppError {
|
||||
constructor(message: string = 'Unauthorized') {
|
||||
constructor(message: string = "Unauthorized") {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends AppError {
|
||||
constructor(message: string = 'Forbidden') {
|
||||
constructor(message: string = "Forbidden") {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
@@ -601,21 +620,21 @@ export class ConflictError extends AppError {
|
||||
|
||||
```typescript
|
||||
// middleware/error-handler.ts
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { AppError } from '../utils/errors';
|
||||
import { logger } from './logger.middleware';
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AppError } from "../utils/errors";
|
||||
import { logger } from "./logger.middleware";
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
next: NextFunction,
|
||||
) => {
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.statusCode).json({
|
||||
status: 'error',
|
||||
status: "error",
|
||||
message: err.message,
|
||||
...(err instanceof ValidationError && { errors: err.errors })
|
||||
...(err instanceof ValidationError && { errors: err.errors }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -624,23 +643,24 @@ export const errorHandler = (
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method
|
||||
method: req.method,
|
||||
});
|
||||
|
||||
// Don't leak error details in production
|
||||
const message = process.env.NODE_ENV === 'production'
|
||||
? 'Internal server error'
|
||||
: err.message;
|
||||
const message =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "Internal server error"
|
||||
: err.message;
|
||||
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
// Async error wrapper
|
||||
export const asyncHandler = (
|
||||
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
|
||||
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
|
||||
) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
@@ -654,11 +674,11 @@ export const asyncHandler = (
|
||||
|
||||
```typescript
|
||||
// config/database.ts
|
||||
import { Pool, PoolConfig } from 'pg';
|
||||
import { Pool, PoolConfig } from "pg";
|
||||
|
||||
const poolConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
@@ -670,19 +690,19 @@ const poolConfig: PoolConfig = {
|
||||
export const pool = new Pool(poolConfig);
|
||||
|
||||
// Test connection
|
||||
pool.on('connect', () => {
|
||||
console.log('Database connected');
|
||||
pool.on("connect", () => {
|
||||
console.log("Database connected");
|
||||
});
|
||||
|
||||
pool.on('error', (err) => {
|
||||
console.error('Unexpected database error', err);
|
||||
pool.on("error", (err) => {
|
||||
console.error("Unexpected database error", err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
export const closeDatabase = async () => {
|
||||
await pool.end();
|
||||
console.log('Database connection closed');
|
||||
console.log("Database connection closed");
|
||||
};
|
||||
```
|
||||
|
||||
@@ -690,7 +710,7 @@ export const closeDatabase = async () => {
|
||||
|
||||
```typescript
|
||||
// config/mongoose.ts
|
||||
import mongoose from 'mongoose';
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
@@ -700,25 +720,25 @@ const connectDB = async () => {
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
|
||||
console.log('MongoDB connected');
|
||||
console.log("MongoDB connected");
|
||||
} catch (error) {
|
||||
console.error('MongoDB connection error:', error);
|
||||
console.error("MongoDB connection error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
mongoose.connection.on('disconnected', () => {
|
||||
console.log('MongoDB disconnected');
|
||||
mongoose.connection.on("disconnected", () => {
|
||||
console.log("MongoDB disconnected");
|
||||
});
|
||||
|
||||
mongoose.connection.on('error', (err) => {
|
||||
console.error('MongoDB error:', err);
|
||||
mongoose.connection.on("error", (err) => {
|
||||
console.error("MongoDB error:", err);
|
||||
});
|
||||
|
||||
export { connectDB };
|
||||
|
||||
// Model example
|
||||
import { Schema, model, Document } from 'mongoose';
|
||||
import { Schema, model, Document } from "mongoose";
|
||||
|
||||
interface IUser extends Document {
|
||||
name: string;
|
||||
@@ -728,25 +748,28 @@ interface IUser extends Document {
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>({
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
}, {
|
||||
timestamps: true
|
||||
});
|
||||
const userSchema = new Schema<IUser>(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Indexes
|
||||
userSchema.index({ email: 1 });
|
||||
|
||||
export const User = model<IUser>('User', userSchema);
|
||||
export const User = model<IUser>("User", userSchema);
|
||||
```
|
||||
|
||||
### Transaction Pattern
|
||||
|
||||
```typescript
|
||||
// services/order.service.ts
|
||||
import { Pool } from 'pg';
|
||||
import { Pool } from "pg";
|
||||
|
||||
export class OrderService {
|
||||
constructor(private db: Pool) {}
|
||||
@@ -755,33 +778,33 @@ export class OrderService {
|
||||
const client = await this.db.connect();
|
||||
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Create order
|
||||
const orderResult = await client.query(
|
||||
'INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id',
|
||||
[userId, calculateTotal(items)]
|
||||
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
|
||||
[userId, calculateTotal(items)],
|
||||
);
|
||||
const orderId = orderResult.rows[0].id;
|
||||
|
||||
// Create order items
|
||||
for (const item of items) {
|
||||
await client.query(
|
||||
'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)',
|
||||
[orderId, item.productId, item.quantity, item.price]
|
||||
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",
|
||||
[orderId, item.productId, item.quantity, item.price],
|
||||
);
|
||||
|
||||
// Update inventory
|
||||
await client.query(
|
||||
'UPDATE products SET stock = stock - $1 WHERE id = $2',
|
||||
[item.quantity, item.productId]
|
||||
"UPDATE products SET stock = stock - $1 WHERE id = $2",
|
||||
[item.quantity, item.productId],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
await client.query("COMMIT");
|
||||
return orderId;
|
||||
} catch (error) {
|
||||
await client.query('ROLLBACK');
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
@@ -796,10 +819,10 @@ export class OrderService {
|
||||
|
||||
```typescript
|
||||
// services/auth.service.ts
|
||||
import jwt from 'jsonwebtoken';
|
||||
import bcrypt from 'bcrypt';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
import { UnauthorizedError } from '../utils/errors';
|
||||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from "bcrypt";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
export class AuthService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
@@ -808,22 +831,22 @@ export class AuthService {
|
||||
const user = await this.userRepository.findByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError('Invalid credentials');
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedError('Invalid credentials');
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
const refreshToken = this.generateRefreshToken({
|
||||
userId: user.id
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -832,8 +855,8 @@ export class AuthService {
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email
|
||||
}
|
||||
email: user.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -841,35 +864,35 @@ export class AuthService {
|
||||
try {
|
||||
const payload = jwt.verify(
|
||||
refreshToken,
|
||||
process.env.REFRESH_TOKEN_SECRET!
|
||||
process.env.REFRESH_TOKEN_SECRET!,
|
||||
) as { userId: string };
|
||||
|
||||
const user = await this.userRepository.findById(payload.userId);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError('User not found');
|
||||
throw new UnauthorizedError("User not found");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
return { token };
|
||||
} catch (error) {
|
||||
throw new UnauthorizedError('Invalid refresh token');
|
||||
throw new UnauthorizedError("Invalid refresh token");
|
||||
}
|
||||
}
|
||||
|
||||
private generateToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.JWT_SECRET!, {
|
||||
expiresIn: '15m'
|
||||
expiresIn: "15m",
|
||||
});
|
||||
}
|
||||
|
||||
private generateRefreshToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
|
||||
expiresIn: '7d'
|
||||
expiresIn: "7d",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -879,15 +902,15 @@ export class AuthService {
|
||||
|
||||
```typescript
|
||||
// utils/cache.ts
|
||||
import Redis from 'ioredis';
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
retryStrategy: (times) => {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export class CacheService {
|
||||
@@ -922,7 +945,7 @@ export function Cacheable(ttl: number = 300) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
@@ -950,22 +973,27 @@ export function Cacheable(ttl: number = 300) {
|
||||
|
||||
```typescript
|
||||
// utils/response.ts
|
||||
import { Response } from 'express';
|
||||
import { Response } from "express";
|
||||
|
||||
export class ApiResponse {
|
||||
static success<T>(res: Response, data: T, message?: string, statusCode = 200) {
|
||||
static success<T>(
|
||||
res: Response,
|
||||
data: T,
|
||||
message?: string,
|
||||
statusCode = 200,
|
||||
) {
|
||||
return res.status(statusCode).json({
|
||||
status: 'success',
|
||||
status: "success",
|
||||
message,
|
||||
data
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
static error(res: Response, message: string, statusCode = 500, errors?: any) {
|
||||
return res.status(statusCode).json({
|
||||
status: 'error',
|
||||
status: "error",
|
||||
message,
|
||||
...(errors && { errors })
|
||||
...(errors && { errors }),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -974,17 +1002,17 @@ export class ApiResponse {
|
||||
data: T[],
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number
|
||||
total: number,
|
||||
) {
|
||||
return res.json({
|
||||
status: 'success',
|
||||
status: "success",
|
||||
data,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit)
|
||||
}
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,17 +25,19 @@ Comprehensive guidance for mastering TypeScript's advanced type system including
|
||||
**Purpose:** Create reusable, type-flexible components while maintaining type safety.
|
||||
|
||||
**Basic Generic Function:**
|
||||
|
||||
```typescript
|
||||
function identity<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
|
||||
const num = identity<number>(42); // Type: number
|
||||
const str = identity<string>("hello"); // Type: string
|
||||
const auto = identity(true); // Type inferred: boolean
|
||||
const num = identity<number>(42); // Type: number
|
||||
const str = identity<string>("hello"); // Type: string
|
||||
const auto = identity(true); // Type inferred: boolean
|
||||
```
|
||||
|
||||
**Generic Constraints:**
|
||||
|
||||
```typescript
|
||||
interface HasLength {
|
||||
length: number;
|
||||
@@ -46,22 +48,20 @@ function logLength<T extends HasLength>(item: T): T {
|
||||
return item;
|
||||
}
|
||||
|
||||
logLength("hello"); // OK: string has length
|
||||
logLength([1, 2, 3]); // OK: array has length
|
||||
logLength({ length: 10 }); // OK: object has length
|
||||
logLength("hello"); // OK: string has length
|
||||
logLength([1, 2, 3]); // OK: array has length
|
||||
logLength({ length: 10 }); // OK: object has length
|
||||
// logLength(42); // Error: number has no length
|
||||
```
|
||||
|
||||
**Multiple Type Parameters:**
|
||||
|
||||
```typescript
|
||||
function merge<T, U>(obj1: T, obj2: U): T & U {
|
||||
return { ...obj1, ...obj2 };
|
||||
}
|
||||
|
||||
const merged = merge(
|
||||
{ name: "John" },
|
||||
{ age: 30 }
|
||||
);
|
||||
const merged = merge({ name: "John" }, { age: 30 });
|
||||
// Type: { name: string } & { age: number }
|
||||
```
|
||||
|
||||
@@ -70,14 +70,16 @@ const merged = merge(
|
||||
**Purpose:** Create types that depend on conditions, enabling sophisticated type logic.
|
||||
|
||||
**Basic Conditional Type:**
|
||||
|
||||
```typescript
|
||||
type IsString<T> = T extends string ? true : false;
|
||||
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
type A = IsString<string>; // true
|
||||
type B = IsString<number>; // false
|
||||
```
|
||||
|
||||
**Extracting Return Types:**
|
||||
|
||||
```typescript
|
||||
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
|
||||
@@ -90,6 +92,7 @@ type User = ReturnType<typeof getUser>;
|
||||
```
|
||||
|
||||
**Distributive Conditional Types:**
|
||||
|
||||
```typescript
|
||||
type ToArray<T> = T extends any ? T[] : never;
|
||||
|
||||
@@ -98,16 +101,21 @@ type StrOrNumArray = ToArray<string | number>;
|
||||
```
|
||||
|
||||
**Nested Conditions:**
|
||||
```typescript
|
||||
type TypeName<T> =
|
||||
T extends string ? "string" :
|
||||
T extends number ? "number" :
|
||||
T extends boolean ? "boolean" :
|
||||
T extends undefined ? "undefined" :
|
||||
T extends Function ? "function" :
|
||||
"object";
|
||||
|
||||
type T1 = TypeName<string>; // "string"
|
||||
```typescript
|
||||
type TypeName<T> = T extends string
|
||||
? "string"
|
||||
: T extends number
|
||||
? "number"
|
||||
: T extends boolean
|
||||
? "boolean"
|
||||
: T extends undefined
|
||||
? "undefined"
|
||||
: T extends Function
|
||||
? "function"
|
||||
: "object";
|
||||
|
||||
type T1 = TypeName<string>; // "string"
|
||||
type T2 = TypeName<() => void>; // "function"
|
||||
```
|
||||
|
||||
@@ -116,6 +124,7 @@ type T2 = TypeName<() => void>; // "function"
|
||||
**Purpose:** Transform existing types by iterating over their properties.
|
||||
|
||||
**Basic Mapped Type:**
|
||||
|
||||
```typescript
|
||||
type Readonly<T> = {
|
||||
readonly [P in keyof T]: T[P];
|
||||
@@ -131,6 +140,7 @@ type ReadonlyUser = Readonly<User>;
|
||||
```
|
||||
|
||||
**Optional Properties:**
|
||||
|
||||
```typescript
|
||||
type Partial<T> = {
|
||||
[P in keyof T]?: T[P];
|
||||
@@ -141,9 +151,10 @@ type PartialUser = Partial<User>;
|
||||
```
|
||||
|
||||
**Key Remapping:**
|
||||
|
||||
```typescript
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
interface Person {
|
||||
@@ -156,9 +167,10 @@ type PersonGetters = Getters<Person>;
|
||||
```
|
||||
|
||||
**Filtering Properties:**
|
||||
|
||||
```typescript
|
||||
type PickByType<T, U> = {
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K]
|
||||
[K in keyof T as T[K] extends U ? K : never]: T[K];
|
||||
};
|
||||
|
||||
interface Mixed {
|
||||
@@ -177,6 +189,7 @@ type OnlyNumbers = PickByType<Mixed, number>;
|
||||
**Purpose:** Create string-based types with pattern matching and transformation.
|
||||
|
||||
**Basic Template Literal:**
|
||||
|
||||
```typescript
|
||||
type EventName = "click" | "focus" | "blur";
|
||||
type EventHandler = `on${Capitalize<EventName>}`;
|
||||
@@ -184,19 +197,20 @@ type EventHandler = `on${Capitalize<EventName>}`;
|
||||
```
|
||||
|
||||
**String Manipulation:**
|
||||
|
||||
```typescript
|
||||
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
|
||||
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
|
||||
type CapitalizedName = Capitalize<"john">; // "John"
|
||||
type UppercaseGreeting = Uppercase<"hello">; // "HELLO"
|
||||
type LowercaseGreeting = Lowercase<"HELLO">; // "hello"
|
||||
type CapitalizedName = Capitalize<"john">; // "John"
|
||||
type UncapitalizedName = Uncapitalize<"John">; // "john"
|
||||
```
|
||||
|
||||
**Path Building:**
|
||||
|
||||
```typescript
|
||||
type Path<T> = T extends object
|
||||
? { [K in keyof T]: K extends string
|
||||
? `${K}` | `${K}.${Path<T[K]>}`
|
||||
: never
|
||||
? {
|
||||
[K in keyof T]: K extends string ? `${K}` | `${K}.${Path<T[K]>}` : never;
|
||||
}[keyof T]
|
||||
: never;
|
||||
|
||||
@@ -235,13 +249,13 @@ type UserName = Pick<User, "name" | "email">;
|
||||
type UserWithoutPassword = Omit<User, "password">;
|
||||
|
||||
// Exclude<T, U> - Exclude types from union
|
||||
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
|
||||
type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
|
||||
|
||||
// Extract<T, U> - Extract types from union
|
||||
type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
|
||||
type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
|
||||
|
||||
// NonNullable<T> - Exclude null and undefined
|
||||
type T3 = NonNullable<string | null | undefined>; // string
|
||||
type T3 = NonNullable<string | null | undefined>; // string
|
||||
|
||||
// Record<K, T> - Create object type with keys K and values T
|
||||
type PageInfo = Record<"home" | "about", { title: string }>;
|
||||
@@ -273,7 +287,7 @@ class TypedEventEmitter<T extends Record<string, any>> {
|
||||
emit<K extends keyof T>(event: K, data: T[K]): void {
|
||||
const callbacks = this.listeners[event];
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => callback(data));
|
||||
callbacks.forEach((callback) => callback(data));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -281,7 +295,7 @@ class TypedEventEmitter<T extends Record<string, any>> {
|
||||
const emitter = new TypedEventEmitter<EventMap>();
|
||||
|
||||
emitter.on("user:created", (data) => {
|
||||
console.log(data.id, data.name); // Type-safe!
|
||||
console.log(data.id, data.name); // Type-safe!
|
||||
});
|
||||
|
||||
emitter.emit("user:created", { id: "1", name: "John" });
|
||||
@@ -310,20 +324,19 @@ type ExtractBody<T> = T extends { body: infer B } ? B : never;
|
||||
type ExtractResponse<T> = T extends { response: infer R } ? R : never;
|
||||
|
||||
class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> {
|
||||
async request<
|
||||
Path extends keyof Config,
|
||||
Method extends keyof Config[Path]
|
||||
>(
|
||||
async request<Path extends keyof Config, Method extends keyof Config[Path]>(
|
||||
path: Path,
|
||||
method: Method,
|
||||
...[options]: ExtractParams<Config[Path][Method]> extends never
|
||||
? ExtractBody<Config[Path][Method]> extends never
|
||||
? []
|
||||
: [{ body: ExtractBody<Config[Path][Method]> }]
|
||||
: [{
|
||||
params: ExtractParams<Config[Path][Method]>;
|
||||
body?: ExtractBody<Config[Path][Method]>;
|
||||
}]
|
||||
: [
|
||||
{
|
||||
params: ExtractParams<Config[Path][Method]>;
|
||||
body?: ExtractBody<Config[Path][Method]>;
|
||||
},
|
||||
]
|
||||
): Promise<ExtractResponse<Config[Path][Method]>> {
|
||||
// Implementation here
|
||||
return {} as any;
|
||||
@@ -337,12 +350,12 @@ const users = await api.request("/users", "GET");
|
||||
// Type: User[]
|
||||
|
||||
const newUser = await api.request("/users", "POST", {
|
||||
body: { name: "John", email: "john@example.com" }
|
||||
body: { name: "John", email: "john@example.com" },
|
||||
});
|
||||
// Type: User
|
||||
|
||||
const user = await api.request("/users/:id", "GET", {
|
||||
params: { id: "123" }
|
||||
params: { id: "123" },
|
||||
});
|
||||
// Type: User
|
||||
```
|
||||
@@ -372,17 +385,12 @@ type IsComplete<T, S> =
|
||||
class Builder<T, S extends BuilderState<T> = {}> {
|
||||
private state: S = {} as S;
|
||||
|
||||
set<K extends keyof T>(
|
||||
key: K,
|
||||
value: T[K]
|
||||
): Builder<T, S & Record<K, T[K]>> {
|
||||
set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> {
|
||||
this.state[key] = value;
|
||||
return this as any;
|
||||
}
|
||||
|
||||
build(
|
||||
this: IsComplete<T, S> extends true ? this : never
|
||||
): T {
|
||||
build(this: IsComplete<T, S> extends true ? this : never): T {
|
||||
return this.state as T;
|
||||
}
|
||||
}
|
||||
@@ -400,7 +408,7 @@ const user = builder
|
||||
.set("id", "1")
|
||||
.set("name", "John")
|
||||
.set("email", "john@example.com")
|
||||
.build(); // OK: all required fields set
|
||||
.build(); // OK: all required fields set
|
||||
|
||||
// const incomplete = builder
|
||||
// .set("id", "1")
|
||||
@@ -507,24 +515,24 @@ const validator = new FormValidator<LoginForm>({
|
||||
email: [
|
||||
{
|
||||
validate: (v) => v.includes("@"),
|
||||
message: "Email must contain @"
|
||||
message: "Email must contain @",
|
||||
},
|
||||
{
|
||||
validate: (v) => v.length > 0,
|
||||
message: "Email is required"
|
||||
}
|
||||
message: "Email is required",
|
||||
},
|
||||
],
|
||||
password: [
|
||||
{
|
||||
validate: (v) => v.length >= 8,
|
||||
message: "Password must be at least 8 characters"
|
||||
}
|
||||
]
|
||||
message: "Password must be at least 8 characters",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const errors = validator.validate({
|
||||
email: "invalid",
|
||||
password: "short"
|
||||
password: "short",
|
||||
});
|
||||
// Type: { email?: string[]; password?: string[]; } | null
|
||||
```
|
||||
@@ -551,10 +559,10 @@ type AsyncState<T> = Success<T> | Error | Loading;
|
||||
function handleState<T>(state: AsyncState<T>): void {
|
||||
switch (state.status) {
|
||||
case "success":
|
||||
console.log(state.data); // Type: T
|
||||
console.log(state.data); // Type: T
|
||||
break;
|
||||
case "error":
|
||||
console.log(state.error); // Type: string
|
||||
console.log(state.error); // Type: string
|
||||
break;
|
||||
case "loading":
|
||||
console.log("Loading...");
|
||||
@@ -605,18 +613,18 @@ function reducer(state: State, event: Event): State {
|
||||
type ElementType<T> = T extends (infer U)[] ? U : never;
|
||||
|
||||
type NumArray = number[];
|
||||
type Num = ElementType<NumArray>; // number
|
||||
type Num = ElementType<NumArray>; // number
|
||||
|
||||
// Extract promise type
|
||||
type PromiseType<T> = T extends Promise<infer U> ? U : never;
|
||||
|
||||
type AsyncNum = PromiseType<Promise<number>>; // number
|
||||
type AsyncNum = PromiseType<Promise<number>>; // number
|
||||
|
||||
// Extract function parameters
|
||||
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
|
||||
|
||||
function foo(a: string, b: number) {}
|
||||
type FooParams = Parameters<typeof foo>; // [string, number]
|
||||
type FooParams = Parameters<typeof foo>; // [string, number]
|
||||
```
|
||||
|
||||
### 2. Type Guards
|
||||
@@ -628,7 +636,7 @@ function isString(value: unknown): value is string {
|
||||
|
||||
function isArrayOf<T>(
|
||||
value: unknown,
|
||||
guard: (item: unknown) => item is T
|
||||
guard: (item: unknown) => item is T,
|
||||
): value is T[] {
|
||||
return Array.isArray(value) && value.every(guard);
|
||||
}
|
||||
@@ -636,7 +644,7 @@ function isArrayOf<T>(
|
||||
const data: unknown = ["a", "b", "c"];
|
||||
|
||||
if (isArrayOf(data, isString)) {
|
||||
data.forEach(s => s.toUpperCase()); // Type: string[]
|
||||
data.forEach((s) => s.toUpperCase()); // Type: string[]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -673,15 +681,14 @@ function processValue(value: unknown) {
|
||||
|
||||
```typescript
|
||||
// Type assertion tests
|
||||
type AssertEqual<T, U> =
|
||||
[T] extends [U]
|
||||
? [U] extends [T]
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
type AssertEqual<T, U> = [T] extends [U]
|
||||
? [U] extends [T]
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
|
||||
type Test1 = AssertEqual<string, string>; // true
|
||||
type Test2 = AssertEqual<string, number>; // false
|
||||
type Test1 = AssertEqual<string, string>; // true
|
||||
type Test2 = AssertEqual<string, number>; // false
|
||||
type Test3 = AssertEqual<string | number, string>; // false
|
||||
|
||||
// Expect error helper
|
||||
|
||||
Reference in New Issue
Block a user