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:
@@ -23,11 +23,13 @@ Build secure, scalable authentication and authorization systems using industry-s
|
||||
### 1. Authentication vs Authorization
|
||||
|
||||
**Authentication (AuthN)**: Who are you?
|
||||
|
||||
- Verifying identity (username/password, OAuth, biometrics)
|
||||
- Issuing credentials (sessions, tokens)
|
||||
- Managing login/logout
|
||||
|
||||
**Authorization (AuthZ)**: What can you do?
|
||||
|
||||
- Permission checking
|
||||
- Role-based access control (RBAC)
|
||||
- Resource ownership validation
|
||||
@@ -36,16 +38,19 @@ Build secure, scalable authentication and authorization systems using industry-s
|
||||
### 2. Authentication Strategies
|
||||
|
||||
**Session-Based:**
|
||||
|
||||
- Server stores session state
|
||||
- Session ID in cookie
|
||||
- Traditional, simple, stateful
|
||||
|
||||
**Token-Based (JWT):**
|
||||
|
||||
- Stateless, self-contained
|
||||
- Scales horizontally
|
||||
- Can store claims
|
||||
|
||||
**OAuth2/OpenID Connect:**
|
||||
|
||||
- Delegate authentication
|
||||
- Social login (Google, GitHub)
|
||||
- Enterprise SSO
|
||||
@@ -56,69 +61,69 @@ Build secure, scalable authentication and authorization systems using industry-s
|
||||
|
||||
```typescript
|
||||
// JWT structure: header.payload.signature
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from "jsonwebtoken";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
userId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
iat: number;
|
||||
exp: number;
|
||||
}
|
||||
|
||||
// Generate JWT
|
||||
function generateTokens(userId: string, email: string, role: string) {
|
||||
const accessToken = jwt.sign(
|
||||
{ userId, email, role },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: '15m' } // Short-lived
|
||||
);
|
||||
const accessToken = jwt.sign(
|
||||
{ userId, email, role },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: "15m" }, // Short-lived
|
||||
);
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_REFRESH_SECRET!,
|
||||
{ expiresIn: '7d' } // Long-lived
|
||||
);
|
||||
const refreshToken = jwt.sign(
|
||||
{ userId },
|
||||
process.env.JWT_REFRESH_SECRET!,
|
||||
{ expiresIn: "7d" }, // Long-lived
|
||||
);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
// Verify JWT
|
||||
function verifyToken(token: string): JWTPayload {
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new Error('Token expired');
|
||||
}
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
throw error;
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
||||
} catch (error) {
|
||||
if (error instanceof jwt.TokenExpiredError) {
|
||||
throw new Error("Token expired");
|
||||
}
|
||||
if (error instanceof jwt.JsonWebTokenError) {
|
||||
throw new Error("Invalid token");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Middleware
|
||||
function authenticate(req: Request, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!authHeader?.startsWith("Bearer ")) {
|
||||
return res.status(401).json({ error: "No token provided" });
|
||||
}
|
||||
|
||||
const token = authHeader.substring(7);
|
||||
try {
|
||||
const payload = verifyToken(token);
|
||||
req.user = payload; // Attach user to request
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
const token = authHeader.substring(7);
|
||||
try {
|
||||
const payload = verifyToken(token);
|
||||
req.user = payload; // Attach user to request
|
||||
next();
|
||||
} catch (error) {
|
||||
return res.status(401).json({ error: "Invalid token" });
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.get('/api/profile', authenticate, (req, res) => {
|
||||
res.json({ user: req.user });
|
||||
app.get("/api/profile", authenticate, (req, res) => {
|
||||
res.json({ user: req.user });
|
||||
});
|
||||
```
|
||||
|
||||
@@ -126,94 +131,93 @@ app.get('/api/profile', authenticate, (req, res) => {
|
||||
|
||||
```typescript
|
||||
interface StoredRefreshToken {
|
||||
token: string;
|
||||
userId: string;
|
||||
expiresAt: Date;
|
||||
createdAt: Date;
|
||||
token: string;
|
||||
userId: string;
|
||||
expiresAt: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
class RefreshTokenService {
|
||||
// Store refresh token in database
|
||||
async storeRefreshToken(userId: string, refreshToken: string) {
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
await db.refreshTokens.create({
|
||||
token: await hash(refreshToken), // Hash before storing
|
||||
userId,
|
||||
expiresAt,
|
||||
});
|
||||
// Store refresh token in database
|
||||
async storeRefreshToken(userId: string, refreshToken: string) {
|
||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
||||
await db.refreshTokens.create({
|
||||
token: await hash(refreshToken), // Hash before storing
|
||||
userId,
|
||||
expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh access token
|
||||
async refreshAccessToken(refreshToken: string) {
|
||||
// Verify refresh token
|
||||
let payload;
|
||||
try {
|
||||
payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as {
|
||||
userId: string;
|
||||
};
|
||||
} catch {
|
||||
throw new Error("Invalid refresh token");
|
||||
}
|
||||
|
||||
// Refresh access token
|
||||
async refreshAccessToken(refreshToken: string) {
|
||||
// Verify refresh token
|
||||
let payload;
|
||||
try {
|
||||
payload = jwt.verify(
|
||||
refreshToken,
|
||||
process.env.JWT_REFRESH_SECRET!
|
||||
) as { userId: string };
|
||||
} catch {
|
||||
throw new Error('Invalid refresh token');
|
||||
}
|
||||
// Check if token exists in database
|
||||
const storedToken = await db.refreshTokens.findOne({
|
||||
where: {
|
||||
token: await hash(refreshToken),
|
||||
userId: payload.userId,
|
||||
expiresAt: { $gt: new Date() },
|
||||
},
|
||||
});
|
||||
|
||||
// Check if token exists in database
|
||||
const storedToken = await db.refreshTokens.findOne({
|
||||
where: {
|
||||
token: await hash(refreshToken),
|
||||
userId: payload.userId,
|
||||
expiresAt: { $gt: new Date() },
|
||||
},
|
||||
});
|
||||
|
||||
if (!storedToken) {
|
||||
throw new Error('Refresh token not found or expired');
|
||||
}
|
||||
|
||||
// Get user
|
||||
const user = await db.users.findById(payload.userId);
|
||||
if (!user) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
// Generate new access token
|
||||
const accessToken = jwt.sign(
|
||||
{ userId: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: '15m' }
|
||||
);
|
||||
|
||||
return { accessToken };
|
||||
if (!storedToken) {
|
||||
throw new Error("Refresh token not found or expired");
|
||||
}
|
||||
|
||||
// Revoke refresh token (logout)
|
||||
async revokeRefreshToken(refreshToken: string) {
|
||||
await db.refreshTokens.deleteOne({
|
||||
token: await hash(refreshToken),
|
||||
});
|
||||
// Get user
|
||||
const user = await db.users.findById(payload.userId);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
// Revoke all user tokens (logout all devices)
|
||||
async revokeAllUserTokens(userId: string) {
|
||||
await db.refreshTokens.deleteMany({ userId });
|
||||
}
|
||||
// Generate new access token
|
||||
const accessToken = jwt.sign(
|
||||
{ userId: user.id, email: user.email, role: user.role },
|
||||
process.env.JWT_SECRET!,
|
||||
{ expiresIn: "15m" },
|
||||
);
|
||||
|
||||
return { accessToken };
|
||||
}
|
||||
|
||||
// Revoke refresh token (logout)
|
||||
async revokeRefreshToken(refreshToken: string) {
|
||||
await db.refreshTokens.deleteOne({
|
||||
token: await hash(refreshToken),
|
||||
});
|
||||
}
|
||||
|
||||
// Revoke all user tokens (logout all devices)
|
||||
async revokeAllUserTokens(userId: string) {
|
||||
await db.refreshTokens.deleteMany({ userId });
|
||||
}
|
||||
}
|
||||
|
||||
// API endpoints
|
||||
app.post('/api/auth/refresh', async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
try {
|
||||
const { accessToken } = await refreshTokenService
|
||||
.refreshAccessToken(refreshToken);
|
||||
res.json({ accessToken });
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid refresh token' });
|
||||
}
|
||||
app.post("/api/auth/refresh", async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
try {
|
||||
const { accessToken } =
|
||||
await refreshTokenService.refreshAccessToken(refreshToken);
|
||||
res.json({ accessToken });
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: "Invalid refresh token" });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/logout', authenticate, async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
await refreshTokenService.revokeRefreshToken(refreshToken);
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
app.post("/api/auth/logout", authenticate, async (req, res) => {
|
||||
const { refreshToken } = req.body;
|
||||
await refreshTokenService.revokeRefreshToken(refreshToken);
|
||||
res.json({ message: "Logged out successfully" });
|
||||
});
|
||||
```
|
||||
|
||||
@@ -222,70 +226,70 @@ app.post('/api/auth/logout', authenticate, async (req, res) => {
|
||||
### Pattern 1: Express Session
|
||||
|
||||
```typescript
|
||||
import session from 'express-session';
|
||||
import RedisStore from 'connect-redis';
|
||||
import { createClient } from 'redis';
|
||||
import session from "express-session";
|
||||
import RedisStore from "connect-redis";
|
||||
import { createClient } from "redis";
|
||||
|
||||
// Setup Redis for session storage
|
||||
const redisClient = createClient({
|
||||
url: process.env.REDIS_URL,
|
||||
url: process.env.REDIS_URL,
|
||||
});
|
||||
await redisClient.connect();
|
||||
|
||||
app.use(
|
||||
session({
|
||||
store: new RedisStore({ client: redisClient }),
|
||||
secret: process.env.SESSION_SECRET!,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production', // HTTPS only
|
||||
httpOnly: true, // No JavaScript access
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
sameSite: 'strict', // CSRF protection
|
||||
},
|
||||
})
|
||||
session({
|
||||
store: new RedisStore({ client: redisClient }),
|
||||
secret: process.env.SESSION_SECRET!,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === "production", // HTTPS only
|
||||
httpOnly: true, // No JavaScript access
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
sameSite: "strict", // CSRF protection
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Login
|
||||
app.post('/api/auth/login', async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
app.post("/api/auth/login", async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await db.users.findOne({ email });
|
||||
if (!user || !(await verifyPassword(password, user.passwordHash))) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
const user = await db.users.findOne({ email });
|
||||
if (!user || !(await verifyPassword(password, user.passwordHash))) {
|
||||
return res.status(401).json({ error: "Invalid credentials" });
|
||||
}
|
||||
|
||||
// Store user in session
|
||||
req.session.userId = user.id;
|
||||
req.session.role = user.role;
|
||||
// Store user in session
|
||||
req.session.userId = user.id;
|
||||
req.session.role = user.role;
|
||||
|
||||
res.json({ user: { id: user.id, email: user.email, role: user.role } });
|
||||
res.json({ user: { id: user.id, email: user.email, role: user.role } });
|
||||
});
|
||||
|
||||
// Session middleware
|
||||
function requireAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
next();
|
||||
if (!req.session.userId) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
// Protected route
|
||||
app.get('/api/profile', requireAuth, async (req, res) => {
|
||||
const user = await db.users.findById(req.session.userId);
|
||||
res.json({ user });
|
||||
app.get("/api/profile", requireAuth, async (req, res) => {
|
||||
const user = await db.users.findById(req.session.userId);
|
||||
res.json({ user });
|
||||
});
|
||||
|
||||
// Logout
|
||||
app.post('/api/auth/logout', (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Logout failed' });
|
||||
}
|
||||
res.clearCookie('connect.sid');
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
app.post("/api/auth/logout", (req, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: "Logout failed" });
|
||||
}
|
||||
res.clearCookie("connect.sid");
|
||||
res.json({ message: "Logged out successfully" });
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -294,56 +298,61 @@ app.post('/api/auth/logout', (req, res) => {
|
||||
### Pattern 1: OAuth2 with Passport.js
|
||||
|
||||
```typescript
|
||||
import passport from 'passport';
|
||||
import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
|
||||
import { Strategy as GitHubStrategy } from 'passport-github2';
|
||||
import passport from "passport";
|
||||
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
||||
import { Strategy as GitHubStrategy } from "passport-github2";
|
||||
|
||||
// Google OAuth
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
callbackURL: '/api/auth/google/callback',
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
// Find or create user
|
||||
let user = await db.users.findOne({
|
||||
googleId: profile.id,
|
||||
});
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: process.env.GOOGLE_CLIENT_ID!,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||
callbackURL: "/api/auth/google/callback",
|
||||
},
|
||||
async (accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
// Find or create user
|
||||
let user = await db.users.findOne({
|
||||
googleId: profile.id,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await db.users.create({
|
||||
googleId: profile.id,
|
||||
email: profile.emails?.[0]?.value,
|
||||
name: profile.displayName,
|
||||
avatar: profile.photos?.[0]?.value,
|
||||
});
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
} catch (error) {
|
||||
return done(error, undefined);
|
||||
}
|
||||
if (!user) {
|
||||
user = await db.users.create({
|
||||
googleId: profile.id,
|
||||
email: profile.emails?.[0]?.value,
|
||||
name: profile.displayName,
|
||||
avatar: profile.photos?.[0]?.value,
|
||||
});
|
||||
}
|
||||
)
|
||||
|
||||
return done(null, user);
|
||||
} catch (error) {
|
||||
return done(error, undefined);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Routes
|
||||
app.get('/api/auth/google', passport.authenticate('google', {
|
||||
scope: ['profile', 'email'],
|
||||
}));
|
||||
app.get(
|
||||
"/api/auth/google",
|
||||
passport.authenticate("google", {
|
||||
scope: ["profile", "email"],
|
||||
}),
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/api/auth/google/callback',
|
||||
passport.authenticate('google', { session: false }),
|
||||
(req, res) => {
|
||||
// Generate JWT
|
||||
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
|
||||
// Redirect to frontend with token
|
||||
res.redirect(`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`);
|
||||
}
|
||||
"/api/auth/google/callback",
|
||||
passport.authenticate("google", { session: false }),
|
||||
(req, res) => {
|
||||
// Generate JWT
|
||||
const tokens = generateTokens(req.user.id, req.user.email, req.user.role);
|
||||
// Redirect to frontend with token
|
||||
res.redirect(
|
||||
`${process.env.FRONTEND_URL}/auth/callback?token=${tokens.accessToken}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -353,45 +362,46 @@ app.get(
|
||||
|
||||
```typescript
|
||||
enum Role {
|
||||
USER = 'user',
|
||||
MODERATOR = 'moderator',
|
||||
ADMIN = 'admin',
|
||||
USER = "user",
|
||||
MODERATOR = "moderator",
|
||||
ADMIN = "admin",
|
||||
}
|
||||
|
||||
const roleHierarchy: Record<Role, Role[]> = {
|
||||
[Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER],
|
||||
[Role.MODERATOR]: [Role.MODERATOR, Role.USER],
|
||||
[Role.USER]: [Role.USER],
|
||||
[Role.ADMIN]: [Role.ADMIN, Role.MODERATOR, Role.USER],
|
||||
[Role.MODERATOR]: [Role.MODERATOR, Role.USER],
|
||||
[Role.USER]: [Role.USER],
|
||||
};
|
||||
|
||||
function hasRole(userRole: Role, requiredRole: Role): boolean {
|
||||
return roleHierarchy[userRole].includes(requiredRole);
|
||||
return roleHierarchy[userRole].includes(requiredRole);
|
||||
}
|
||||
|
||||
// Middleware
|
||||
function requireRole(...roles: Role[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!roles.some(role => hasRole(req.user.role, role))) {
|
||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||
}
|
||||
if (!roles.some((role) => hasRole(req.user.role, role))) {
|
||||
return res.status(403).json({ error: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.delete('/api/users/:id',
|
||||
authenticate,
|
||||
requireRole(Role.ADMIN),
|
||||
async (req, res) => {
|
||||
// Only admins can delete users
|
||||
await db.users.delete(req.params.id);
|
||||
res.json({ message: 'User deleted' });
|
||||
}
|
||||
app.delete(
|
||||
"/api/users/:id",
|
||||
authenticate,
|
||||
requireRole(Role.ADMIN),
|
||||
async (req, res) => {
|
||||
// Only admins can delete users
|
||||
await db.users.delete(req.params.id);
|
||||
res.json({ message: "User deleted" });
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -399,53 +409,54 @@ app.delete('/api/users/:id',
|
||||
|
||||
```typescript
|
||||
enum Permission {
|
||||
READ_USERS = 'read:users',
|
||||
WRITE_USERS = 'write:users',
|
||||
DELETE_USERS = 'delete:users',
|
||||
READ_POSTS = 'read:posts',
|
||||
WRITE_POSTS = 'write:posts',
|
||||
READ_USERS = "read:users",
|
||||
WRITE_USERS = "write:users",
|
||||
DELETE_USERS = "delete:users",
|
||||
READ_POSTS = "read:posts",
|
||||
WRITE_POSTS = "write:posts",
|
||||
}
|
||||
|
||||
const rolePermissions: Record<Role, Permission[]> = {
|
||||
[Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS],
|
||||
[Role.MODERATOR]: [
|
||||
Permission.READ_POSTS,
|
||||
Permission.WRITE_POSTS,
|
||||
Permission.READ_USERS,
|
||||
],
|
||||
[Role.ADMIN]: Object.values(Permission),
|
||||
[Role.USER]: [Permission.READ_POSTS, Permission.WRITE_POSTS],
|
||||
[Role.MODERATOR]: [
|
||||
Permission.READ_POSTS,
|
||||
Permission.WRITE_POSTS,
|
||||
Permission.READ_USERS,
|
||||
],
|
||||
[Role.ADMIN]: Object.values(Permission),
|
||||
};
|
||||
|
||||
function hasPermission(userRole: Role, permission: Permission): boolean {
|
||||
return rolePermissions[userRole]?.includes(permission) ?? false;
|
||||
return rolePermissions[userRole]?.includes(permission) ?? false;
|
||||
}
|
||||
|
||||
function requirePermission(...permissions: Permission[]) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
const hasAllPermissions = permissions.every(permission =>
|
||||
hasPermission(req.user.role, permission)
|
||||
);
|
||||
const hasAllPermissions = permissions.every((permission) =>
|
||||
hasPermission(req.user.role, permission),
|
||||
);
|
||||
|
||||
if (!hasAllPermissions) {
|
||||
return res.status(403).json({ error: 'Insufficient permissions' });
|
||||
}
|
||||
if (!hasAllPermissions) {
|
||||
return res.status(403).json({ error: "Insufficient permissions" });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.get('/api/users',
|
||||
authenticate,
|
||||
requirePermission(Permission.READ_USERS),
|
||||
async (req, res) => {
|
||||
const users = await db.users.findAll();
|
||||
res.json({ users });
|
||||
}
|
||||
app.get(
|
||||
"/api/users",
|
||||
authenticate,
|
||||
requirePermission(Permission.READ_USERS),
|
||||
async (req, res) => {
|
||||
const users = await db.users.findAll();
|
||||
res.json({ users });
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -454,50 +465,51 @@ app.get('/api/users',
|
||||
```typescript
|
||||
// Check if user owns resource
|
||||
async function requireOwnership(
|
||||
resourceType: 'post' | 'comment',
|
||||
resourceIdParam: string = 'id'
|
||||
resourceType: "post" | "comment",
|
||||
resourceIdParam: string = "id",
|
||||
) {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Not authenticated' });
|
||||
}
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: "Not authenticated" });
|
||||
}
|
||||
|
||||
const resourceId = req.params[resourceIdParam];
|
||||
const resourceId = req.params[resourceIdParam];
|
||||
|
||||
// Admins can access anything
|
||||
if (req.user.role === Role.ADMIN) {
|
||||
return next();
|
||||
}
|
||||
// Admins can access anything
|
||||
if (req.user.role === Role.ADMIN) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check ownership
|
||||
let resource;
|
||||
if (resourceType === 'post') {
|
||||
resource = await db.posts.findById(resourceId);
|
||||
} else if (resourceType === 'comment') {
|
||||
resource = await db.comments.findById(resourceId);
|
||||
}
|
||||
// Check ownership
|
||||
let resource;
|
||||
if (resourceType === "post") {
|
||||
resource = await db.posts.findById(resourceId);
|
||||
} else if (resourceType === "comment") {
|
||||
resource = await db.comments.findById(resourceId);
|
||||
}
|
||||
|
||||
if (!resource) {
|
||||
return res.status(404).json({ error: 'Resource not found' });
|
||||
}
|
||||
if (!resource) {
|
||||
return res.status(404).json({ error: "Resource not found" });
|
||||
}
|
||||
|
||||
if (resource.userId !== req.user.userId) {
|
||||
return res.status(403).json({ error: 'Not authorized' });
|
||||
}
|
||||
if (resource.userId !== req.user.userId) {
|
||||
return res.status(403).json({ error: "Not authorized" });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
app.put('/api/posts/:id',
|
||||
authenticate,
|
||||
requireOwnership('post'),
|
||||
async (req, res) => {
|
||||
// User can only update their own posts
|
||||
const post = await db.posts.update(req.params.id, req.body);
|
||||
res.json({ post });
|
||||
}
|
||||
app.put(
|
||||
"/api/posts/:id",
|
||||
authenticate,
|
||||
requireOwnership("post"),
|
||||
async (req, res) => {
|
||||
// User can only update their own posts
|
||||
const post = await db.posts.update(req.params.id, req.body);
|
||||
res.json({ post });
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@@ -506,99 +518,100 @@ app.put('/api/posts/:id',
|
||||
### Pattern 1: Password Security
|
||||
|
||||
```typescript
|
||||
import bcrypt from 'bcrypt';
|
||||
import { z } from 'zod';
|
||||
import bcrypt from "bcrypt";
|
||||
import { z } from "zod";
|
||||
|
||||
// Password validation schema
|
||||
const passwordSchema = z.string()
|
||||
.min(12, 'Password must be at least 12 characters')
|
||||
.regex(/[A-Z]/, 'Password must contain uppercase letter')
|
||||
.regex(/[a-z]/, 'Password must contain lowercase letter')
|
||||
.regex(/[0-9]/, 'Password must contain number')
|
||||
.regex(/[^A-Za-z0-9]/, 'Password must contain special character');
|
||||
const passwordSchema = z
|
||||
.string()
|
||||
.min(12, "Password must be at least 12 characters")
|
||||
.regex(/[A-Z]/, "Password must contain uppercase letter")
|
||||
.regex(/[a-z]/, "Password must contain lowercase letter")
|
||||
.regex(/[0-9]/, "Password must contain number")
|
||||
.regex(/[^A-Za-z0-9]/, "Password must contain special character");
|
||||
|
||||
// Hash password
|
||||
async function hashPassword(password: string): Promise<string> {
|
||||
const saltRounds = 12; // 2^12 iterations
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
const saltRounds = 12; // 2^12 iterations
|
||||
return bcrypt.hash(password, saltRounds);
|
||||
}
|
||||
|
||||
// Verify password
|
||||
async function verifyPassword(
|
||||
password: string,
|
||||
hash: string
|
||||
password: string,
|
||||
hash: string,
|
||||
): Promise<boolean> {
|
||||
return bcrypt.compare(password, hash);
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
// Registration with password validation
|
||||
app.post('/api/auth/register', async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
app.post("/api/auth/register", async (req, res) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
// Validate password
|
||||
passwordSchema.parse(password);
|
||||
// Validate password
|
||||
passwordSchema.parse(password);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await db.users.findOne({ email });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: 'Email already registered' });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
// Create user
|
||||
const user = await db.users.create({
|
||||
email,
|
||||
passwordHash,
|
||||
});
|
||||
|
||||
// Generate tokens
|
||||
const tokens = generateTokens(user.id, user.email, user.role);
|
||||
|
||||
res.status(201).json({
|
||||
user: { id: user.id, email: user.email },
|
||||
...tokens,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: error.errors[0].message });
|
||||
}
|
||||
res.status(500).json({ error: 'Registration failed' });
|
||||
// Check if user exists
|
||||
const existingUser = await db.users.findOne({ email });
|
||||
if (existingUser) {
|
||||
return res.status(400).json({ error: "Email already registered" });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await hashPassword(password);
|
||||
|
||||
// Create user
|
||||
const user = await db.users.create({
|
||||
email,
|
||||
passwordHash,
|
||||
});
|
||||
|
||||
// Generate tokens
|
||||
const tokens = generateTokens(user.id, user.email, user.role);
|
||||
|
||||
res.status(201).json({
|
||||
user: { id: user.id, email: user.email },
|
||||
...tokens,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return res.status(400).json({ error: error.errors[0].message });
|
||||
}
|
||||
res.status(500).json({ error: "Registration failed" });
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Rate Limiting
|
||||
|
||||
```typescript
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import RedisStore from 'rate-limit-redis';
|
||||
import rateLimit from "express-rate-limit";
|
||||
import RedisStore from "rate-limit-redis";
|
||||
|
||||
// Login rate limiter
|
||||
const loginLimiter = rateLimit({
|
||||
store: new RedisStore({ client: redisClient }),
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // 5 attempts
|
||||
message: 'Too many login attempts, please try again later',
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
store: new RedisStore({ client: redisClient }),
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 5, // 5 attempts
|
||||
message: "Too many login attempts, please try again later",
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
// API rate limiter
|
||||
const apiLimiter = rateLimit({
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 100, // 100 requests per minute
|
||||
standardHeaders: true,
|
||||
windowMs: 60 * 1000, // 1 minute
|
||||
max: 100, // 100 requests per minute
|
||||
standardHeaders: true,
|
||||
});
|
||||
|
||||
// Apply to routes
|
||||
app.post('/api/auth/login', loginLimiter, async (req, res) => {
|
||||
// Login logic
|
||||
app.post("/api/auth/login", loginLimiter, async (req, res) => {
|
||||
// Login logic
|
||||
});
|
||||
|
||||
app.use('/api/', apiLimiter);
|
||||
app.use("/api/", apiLimiter);
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
@@ -39,13 +39,13 @@ workspace/
|
||||
|
||||
### 2. Key Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **Target** | Buildable unit (library, binary, test) |
|
||||
| **Package** | Directory with BUILD file |
|
||||
| **Label** | Target identifier `//path/to:target` |
|
||||
| **Rule** | Defines how to build a target |
|
||||
| **Aspect** | Cross-cutting build behavior |
|
||||
| Concept | Description |
|
||||
| ----------- | -------------------------------------- |
|
||||
| **Target** | Buildable unit (library, binary, test) |
|
||||
| **Package** | Directory with BUILD file |
|
||||
| **Label** | Target identifier `//path/to:target` |
|
||||
| **Rule** | Defines how to build a target |
|
||||
| **Aspect** | Cross-cutting build behavior |
|
||||
|
||||
## Templates
|
||||
|
||||
@@ -366,6 +366,7 @@ bazel build //... --notrack_incremental_state
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- **Use fine-grained targets** - Better caching
|
||||
- **Pin dependencies** - Reproducible builds
|
||||
- **Enable remote caching** - Share build artifacts
|
||||
@@ -373,8 +374,9 @@ bazel build //... --notrack_incremental_state
|
||||
- **Write BUILD files per directory** - Standard convention
|
||||
|
||||
### Don'ts
|
||||
|
||||
- **Don't use glob for deps** - Explicit is better
|
||||
- **Don't commit bazel-* dirs** - Add to .gitignore
|
||||
- **Don't commit bazel-\* dirs** - Add to .gitignore
|
||||
- **Don't skip WORKSPACE setup** - Foundation of build
|
||||
- **Don't ignore build warnings** - Technical debt
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
### 1. The Review Mindset
|
||||
|
||||
**Goals of Code Review:**
|
||||
|
||||
- Catch bugs and edge cases
|
||||
- Ensure code maintainability
|
||||
- Share knowledge across team
|
||||
@@ -31,6 +32,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
- Build team culture
|
||||
|
||||
**Not the Goals:**
|
||||
|
||||
- Show off knowledge
|
||||
- Nitpick formatting (use linters)
|
||||
- Block progress unnecessarily
|
||||
@@ -39,6 +41,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
### 2. Effective Feedback
|
||||
|
||||
**Good Feedback is:**
|
||||
|
||||
- Specific and actionable
|
||||
- Educational, not judgmental
|
||||
- Focused on the code, not the person
|
||||
@@ -48,20 +51,21 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
```markdown
|
||||
❌ Bad: "This is wrong."
|
||||
✅ Good: "This could cause a race condition when multiple users
|
||||
access simultaneously. Consider using a mutex here."
|
||||
access simultaneously. Consider using a mutex here."
|
||||
|
||||
❌ Bad: "Why didn't you use X pattern?"
|
||||
✅ Good: "Have you considered the Repository pattern? It would
|
||||
make this easier to test. Here's an example: [link]"
|
||||
make this easier to test. Here's an example: [link]"
|
||||
|
||||
❌ Bad: "Rename this variable."
|
||||
✅ Good: "[nit] Consider `userCount` instead of `uc` for
|
||||
clarity. Not blocking if you prefer to keep it."
|
||||
clarity. Not blocking if you prefer to keep it."
|
||||
```
|
||||
|
||||
### 3. Review Scope
|
||||
|
||||
**What to Review:**
|
||||
|
||||
- Logic correctness and edge cases
|
||||
- Security vulnerabilities
|
||||
- Performance implications
|
||||
@@ -72,6 +76,7 @@ Transform code reviews from gatekeeping to knowledge sharing through constructiv
|
||||
- Architectural fit
|
||||
|
||||
**What Not to Review Manually:**
|
||||
|
||||
- Code formatting (use Prettier, Black, etc.)
|
||||
- Import organization
|
||||
- Linting violations
|
||||
@@ -159,6 +164,7 @@ For each file:
|
||||
|
||||
```markdown
|
||||
## Security Checklist
|
||||
|
||||
- [ ] User input validated and sanitized
|
||||
- [ ] SQL queries use parameterization
|
||||
- [ ] Authentication/authorization checked
|
||||
@@ -166,6 +172,7 @@ For each file:
|
||||
- [ ] Error messages don't leak info
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
- [ ] No N+1 queries
|
||||
- [ ] Database queries indexed
|
||||
- [ ] Large lists paginated
|
||||
@@ -173,6 +180,7 @@ For each file:
|
||||
- [ ] No blocking I/O in hot paths
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Happy path tested
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Error cases tested
|
||||
@@ -193,28 +201,28 @@ Instead of stating problems, ask questions to encourage thinking:
|
||||
|
||||
❌ "This is inefficient."
|
||||
✅ "I see this loops through all users. Have we considered
|
||||
the performance impact with 100k users?"
|
||||
the performance impact with 100k users?"
|
||||
```
|
||||
|
||||
### Technique 3: Suggest, Don't Command
|
||||
|
||||
```markdown
|
||||
````markdown
|
||||
## Use Collaborative Language
|
||||
|
||||
❌ "You must change this to use async/await"
|
||||
✅ "Suggestion: async/await might make this more readable:
|
||||
```typescript
|
||||
`typescript
|
||||
async function fetchUser(id: string) {
|
||||
const user = await db.query('SELECT * FROM users WHERE id = ?', id);
|
||||
return user;
|
||||
}
|
||||
```
|
||||
What do you think?"
|
||||
`
|
||||
What do you think?"
|
||||
|
||||
❌ "Extract this into a function"
|
||||
✅ "This logic appears in 3 places. Would it make sense to
|
||||
extract it into a shared utility function?"
|
||||
```
|
||||
extract it into a shared utility function?"
|
||||
````
|
||||
|
||||
### Technique 4: Differentiate Severity
|
||||
|
||||
@@ -230,7 +238,7 @@ Use labels to indicate priority:
|
||||
|
||||
Example:
|
||||
"🔴 [blocking] This SQL query is vulnerable to injection.
|
||||
Please use parameterized queries."
|
||||
Please use parameterized queries."
|
||||
|
||||
"🟢 [nit] Consider renaming `data` to `userData` for clarity."
|
||||
|
||||
@@ -389,24 +397,28 @@ test('displays incremented count when clicked', () => {
|
||||
## Security Review Checklist
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- [ ] Is authentication required where needed?
|
||||
- [ ] Are authorization checks before every action?
|
||||
- [ ] Is JWT validation proper (signature, expiry)?
|
||||
- [ ] Are API keys/secrets properly secured?
|
||||
|
||||
### Input Validation
|
||||
|
||||
- [ ] All user inputs validated?
|
||||
- [ ] File uploads restricted (size, type)?
|
||||
- [ ] SQL queries parameterized?
|
||||
- [ ] XSS protection (escape output)?
|
||||
|
||||
### Data Protection
|
||||
|
||||
- [ ] Passwords hashed (bcrypt/argon2)?
|
||||
- [ ] Sensitive data encrypted at rest?
|
||||
- [ ] HTTPS enforced for sensitive data?
|
||||
- [ ] PII handled according to regulations?
|
||||
|
||||
### Common Vulnerabilities
|
||||
|
||||
- [ ] No eval() or similar dynamic execution?
|
||||
- [ ] No hardcoded secrets?
|
||||
- [ ] CSRF protection for state-changing operations?
|
||||
@@ -444,14 +456,14 @@ When author disagrees with your feedback:
|
||||
|
||||
1. **Seek to Understand**
|
||||
"Help me understand your approach. What led you to
|
||||
choose this pattern?"
|
||||
choose this pattern?"
|
||||
|
||||
2. **Acknowledge Valid Points**
|
||||
"That's a good point about X. I hadn't considered that."
|
||||
|
||||
3. **Provide Data**
|
||||
"I'm concerned about performance. Can we add a benchmark
|
||||
to validate the approach?"
|
||||
to validate the approach?"
|
||||
|
||||
4. **Escalate if Needed**
|
||||
"Let's get [architect/senior dev] to weigh in on this."
|
||||
@@ -488,25 +500,31 @@ When author disagrees with your feedback:
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
[Brief overview of what was reviewed]
|
||||
|
||||
## Strengths
|
||||
|
||||
- [What was done well]
|
||||
- [Good patterns or approaches]
|
||||
|
||||
## Required Changes
|
||||
|
||||
🔴 [Blocking issue 1]
|
||||
🔴 [Blocking issue 2]
|
||||
|
||||
## Suggestions
|
||||
|
||||
💡 [Improvement 1]
|
||||
💡 [Improvement 2]
|
||||
|
||||
## Questions
|
||||
|
||||
❓ [Clarification needed on X]
|
||||
❓ [Alternative approach consideration]
|
||||
|
||||
## Verdict
|
||||
|
||||
✅ Approve after addressing required changes
|
||||
```
|
||||
|
||||
|
||||
@@ -31,11 +31,13 @@ Transform debugging from frustrating guesswork into systematic problem-solving w
|
||||
### 2. Debugging Mindset
|
||||
|
||||
**Don't Assume:**
|
||||
|
||||
- "It can't be X" - Yes it can
|
||||
- "I didn't change Y" - Check anyway
|
||||
- "It works on my machine" - Find out why
|
||||
|
||||
**Do:**
|
||||
|
||||
- Reproduce consistently
|
||||
- Isolate the problem
|
||||
- Keep detailed notes
|
||||
@@ -153,58 +155,60 @@ Based on gathered info, ask:
|
||||
```typescript
|
||||
// Chrome DevTools Debugger
|
||||
function processOrder(order: Order) {
|
||||
debugger; // Execution pauses here
|
||||
debugger; // Execution pauses here
|
||||
|
||||
const total = calculateTotal(order);
|
||||
console.log('Total:', total);
|
||||
const total = calculateTotal(order);
|
||||
console.log("Total:", total);
|
||||
|
||||
// Conditional breakpoint
|
||||
if (order.items.length > 10) {
|
||||
debugger; // Only breaks if condition true
|
||||
}
|
||||
// Conditional breakpoint
|
||||
if (order.items.length > 10) {
|
||||
debugger; // Only breaks if condition true
|
||||
}
|
||||
|
||||
return total;
|
||||
return total;
|
||||
}
|
||||
|
||||
// Console debugging techniques
|
||||
console.log('Value:', value); // Basic
|
||||
console.table(arrayOfObjects); // Table format
|
||||
console.time('operation'); /* code */ console.timeEnd('operation'); // Timing
|
||||
console.trace(); // Stack trace
|
||||
console.assert(value > 0, 'Value must be positive'); // Assertion
|
||||
console.log("Value:", value); // Basic
|
||||
console.table(arrayOfObjects); // Table format
|
||||
console.time("operation");
|
||||
/* code */ console.timeEnd("operation"); // Timing
|
||||
console.trace(); // Stack trace
|
||||
console.assert(value > 0, "Value must be positive"); // Assertion
|
||||
|
||||
// Performance profiling
|
||||
performance.mark('start-operation');
|
||||
performance.mark("start-operation");
|
||||
// ... operation code
|
||||
performance.mark('end-operation');
|
||||
performance.measure('operation', 'start-operation', 'end-operation');
|
||||
console.log(performance.getEntriesByType('measure'));
|
||||
performance.mark("end-operation");
|
||||
performance.measure("operation", "start-operation", "end-operation");
|
||||
console.log(performance.getEntriesByType("measure"));
|
||||
```
|
||||
|
||||
**VS Code Debugger Configuration:**
|
||||
|
||||
```json
|
||||
// .vscode/launch.json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Program",
|
||||
"program": "${workspaceFolder}/src/index.ts",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Tests",
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
||||
"args": ["--runInBand", "--no-cache"],
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Program",
|
||||
"program": "${workspaceFolder}/src/index.ts",
|
||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"skipFiles": ["<node_internals>/**"]
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Debug Tests",
|
||||
"program": "${workspaceFolder}/node_modules/jest/bin/jest",
|
||||
"args": ["--runInBand", "--no-cache"],
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -332,14 +336,14 @@ Compare working vs broken:
|
||||
```markdown
|
||||
## What's Different?
|
||||
|
||||
| Aspect | Working | Broken |
|
||||
|--------------|-----------------|-----------------|
|
||||
| Environment | Development | Production |
|
||||
| Node version | 18.16.0 | 18.15.0 |
|
||||
| Data | Empty DB | 1M records |
|
||||
| User | Admin | Regular user |
|
||||
| Browser | Chrome | Safari |
|
||||
| Time | During day | After midnight |
|
||||
| Aspect | Working | Broken |
|
||||
| ------------ | ----------- | -------------- |
|
||||
| Environment | Development | Production |
|
||||
| Node version | 18.16.0 | 18.15.0 |
|
||||
| Data | Empty DB | 1M records |
|
||||
| User | Admin | Regular user |
|
||||
| Browser | Chrome | Safari |
|
||||
| Time | During day | After midnight |
|
||||
|
||||
Hypothesis: Time-based issue? Check timezone handling.
|
||||
```
|
||||
@@ -348,24 +352,28 @@ Hypothesis: Time-based issue? Check timezone handling.
|
||||
|
||||
```typescript
|
||||
// Function call tracing
|
||||
function trace(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
const originalMethod = descriptor.value;
|
||||
function trace(
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = function(...args: any[]) {
|
||||
console.log(`Calling ${propertyKey} with args:`, args);
|
||||
const result = originalMethod.apply(this, args);
|
||||
console.log(`${propertyKey} returned:`, result);
|
||||
return result;
|
||||
};
|
||||
descriptor.value = function (...args: any[]) {
|
||||
console.log(`Calling ${propertyKey} with args:`, args);
|
||||
const result = originalMethod.apply(this, args);
|
||||
console.log(`${propertyKey} returned:`, result);
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
class OrderService {
|
||||
@trace
|
||||
calculateTotal(items: Item[]): number {
|
||||
return items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
@trace
|
||||
calculateTotal(items: Item[]): number {
|
||||
return items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -380,26 +388,27 @@ class OrderService {
|
||||
|
||||
// Node.js memory debugging
|
||||
if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
|
||||
console.warn('High memory usage:', process.memoryUsage());
|
||||
console.warn("High memory usage:", process.memoryUsage());
|
||||
|
||||
// Generate heap dump
|
||||
require('v8').writeHeapSnapshot();
|
||||
// Generate heap dump
|
||||
require("v8").writeHeapSnapshot();
|
||||
}
|
||||
|
||||
// Find memory leaks in tests
|
||||
let beforeMemory: number;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeMemory = process.memoryUsage().heapUsed;
|
||||
beforeMemory = process.memoryUsage().heapUsed;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
const afterMemory = process.memoryUsage().heapUsed;
|
||||
const diff = afterMemory - beforeMemory;
|
||||
const afterMemory = process.memoryUsage().heapUsed;
|
||||
const diff = afterMemory - beforeMemory;
|
||||
|
||||
if (diff > 10 * 1024 * 1024) { // 10MB threshold
|
||||
console.warn(`Possible memory leak: ${diff / 1024 / 1024}MB`);
|
||||
}
|
||||
if (diff > 10 * 1024 * 1024) {
|
||||
// 10MB threshold
|
||||
console.warn(`Possible memory leak: ${diff / 1024 / 1024}MB`);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ Build reliable, fast, and maintainable end-to-end test suites that provide confi
|
||||
### 1. E2E Testing Fundamentals
|
||||
|
||||
**What to Test with E2E:**
|
||||
|
||||
- Critical user journeys (login, checkout, signup)
|
||||
- Complex interactions (drag-and-drop, multi-step forms)
|
||||
- Cross-browser compatibility
|
||||
@@ -30,6 +31,7 @@ Build reliable, fast, and maintainable end-to-end test suites that provide confi
|
||||
- Authentication flows
|
||||
|
||||
**What NOT to Test with E2E:**
|
||||
|
||||
- Unit-level logic (use unit tests)
|
||||
- API contracts (use integration tests)
|
||||
- Edge cases (too slow)
|
||||
@@ -38,6 +40,7 @@ Build reliable, fast, and maintainable end-to-end test suites that provide confi
|
||||
### 2. Test Philosophy
|
||||
|
||||
**The Testing Pyramid:**
|
||||
|
||||
```
|
||||
/\
|
||||
/E2E\ ← Few, focused on critical paths
|
||||
@@ -49,6 +52,7 @@ Build reliable, fast, and maintainable end-to-end test suites that provide confi
|
||||
```
|
||||
|
||||
**Best Practices:**
|
||||
|
||||
- Test user behavior, not implementation
|
||||
- Keep tests independent
|
||||
- Make tests deterministic
|
||||
@@ -61,34 +65,31 @@ Build reliable, fast, and maintainable end-to-end test suites that provide confi
|
||||
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [
|
||||
['html'],
|
||||
['junit', { outputFile: 'results.xml' }],
|
||||
],
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
|
||||
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
|
||||
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
|
||||
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
|
||||
],
|
||||
testDir: "./e2e",
|
||||
timeout: 30000,
|
||||
expect: {
|
||||
timeout: 5000,
|
||||
},
|
||||
fullyParallel: true,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
reporter: [["html"], ["junit", { outputFile: "results.xml" }]],
|
||||
use: {
|
||||
baseURL: "http://localhost:3000",
|
||||
trace: "on-first-retry",
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
||||
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
|
||||
{ name: "mobile", use: { ...devices["iPhone 13"] } },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
@@ -96,59 +97,58 @@ export default defineConfig({
|
||||
|
||||
```typescript
|
||||
// pages/LoginPage.ts
|
||||
import { Page, Locator } from '@playwright/test';
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
readonly emailInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly loginButton: Locator;
|
||||
readonly errorMessage: Locator;
|
||||
readonly page: Page;
|
||||
readonly emailInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly loginButton: Locator;
|
||||
readonly errorMessage: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.emailInput = page.getByLabel('Email');
|
||||
this.passwordInput = page.getByLabel('Password');
|
||||
this.loginButton = page.getByRole('button', { name: 'Login' });
|
||||
this.errorMessage = page.getByRole('alert');
|
||||
}
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.emailInput = page.getByLabel("Email");
|
||||
this.passwordInput = page.getByLabel("Password");
|
||||
this.loginButton = page.getByRole("button", { name: "Login" });
|
||||
this.errorMessage = page.getByRole("alert");
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto('/login');
|
||||
}
|
||||
async goto() {
|
||||
await this.page.goto("/login");
|
||||
}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
await this.emailInput.fill(email);
|
||||
await this.passwordInput.fill(password);
|
||||
await this.loginButton.click();
|
||||
}
|
||||
async login(email: string, password: string) {
|
||||
await this.emailInput.fill(email);
|
||||
await this.passwordInput.fill(password);
|
||||
await this.loginButton.click();
|
||||
}
|
||||
|
||||
async getErrorMessage(): Promise<string> {
|
||||
return await this.errorMessage.textContent() ?? '';
|
||||
}
|
||||
async getErrorMessage(): Promise<string> {
|
||||
return (await this.errorMessage.textContent()) ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
// Test using Page Object
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './pages/LoginPage';
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { LoginPage } from "./pages/LoginPage";
|
||||
|
||||
test('successful login', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('user@example.com', 'password123');
|
||||
test("successful login", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login("user@example.com", "password123");
|
||||
|
||||
await expect(page).toHaveURL('/dashboard');
|
||||
await expect(page.getByRole('heading', { name: 'Dashboard' }))
|
||||
.toBeVisible();
|
||||
await expect(page).toHaveURL("/dashboard");
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
||||
});
|
||||
|
||||
test('failed login shows error', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login('invalid@example.com', 'wrong');
|
||||
test("failed login shows error", async ({ page }) => {
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.login("invalid@example.com", "wrong");
|
||||
|
||||
const error = await loginPage.getErrorMessage();
|
||||
expect(error).toContain('Invalid credentials');
|
||||
const error = await loginPage.getErrorMessage();
|
||||
expect(error).toContain("Invalid credentials");
|
||||
});
|
||||
```
|
||||
|
||||
@@ -156,56 +156,56 @@ test('failed login shows error', async ({ page }) => {
|
||||
|
||||
```typescript
|
||||
// fixtures/test-data.ts
|
||||
import { test as base } from '@playwright/test';
|
||||
import { test as base } from "@playwright/test";
|
||||
|
||||
type TestData = {
|
||||
testUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
};
|
||||
adminUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
testUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
name: string;
|
||||
};
|
||||
adminUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const test = base.extend<TestData>({
|
||||
testUser: async ({}, use) => {
|
||||
const user = {
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
password: 'Test123!@#',
|
||||
name: 'Test User',
|
||||
};
|
||||
// Setup: Create user in database
|
||||
await createTestUser(user);
|
||||
await use(user);
|
||||
// Teardown: Clean up user
|
||||
await deleteTestUser(user.email);
|
||||
},
|
||||
testUser: async ({}, use) => {
|
||||
const user = {
|
||||
email: `test-${Date.now()}@example.com`,
|
||||
password: "Test123!@#",
|
||||
name: "Test User",
|
||||
};
|
||||
// Setup: Create user in database
|
||||
await createTestUser(user);
|
||||
await use(user);
|
||||
// Teardown: Clean up user
|
||||
await deleteTestUser(user.email);
|
||||
},
|
||||
|
||||
adminUser: async ({}, use) => {
|
||||
await use({
|
||||
email: 'admin@example.com',
|
||||
password: process.env.ADMIN_PASSWORD!,
|
||||
});
|
||||
},
|
||||
adminUser: async ({}, use) => {
|
||||
await use({
|
||||
email: "admin@example.com",
|
||||
password: process.env.ADMIN_PASSWORD!,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Usage in tests
|
||||
import { test } from './fixtures/test-data';
|
||||
import { test } from "./fixtures/test-data";
|
||||
|
||||
test('user can update profile', async ({ page, testUser }) => {
|
||||
await page.goto('/login');
|
||||
await page.getByLabel('Email').fill(testUser.email);
|
||||
await page.getByLabel('Password').fill(testUser.password);
|
||||
await page.getByRole('button', { name: 'Login' }).click();
|
||||
test("user can update profile", async ({ page, testUser }) => {
|
||||
await page.goto("/login");
|
||||
await page.getByLabel("Email").fill(testUser.email);
|
||||
await page.getByLabel("Password").fill(testUser.password);
|
||||
await page.getByRole("button", { name: "Login" }).click();
|
||||
|
||||
await page.goto('/profile');
|
||||
await page.getByLabel('Name').fill('Updated Name');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await page.goto("/profile");
|
||||
await page.getByLabel("Name").fill("Updated Name");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
|
||||
await expect(page.getByText('Profile updated')).toBeVisible();
|
||||
await expect(page.getByText("Profile updated")).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
@@ -213,32 +213,32 @@ test('user can update profile', async ({ page, testUser }) => {
|
||||
|
||||
```typescript
|
||||
// ❌ Bad: Fixed timeouts
|
||||
await page.waitForTimeout(3000); // Flaky!
|
||||
await page.waitForTimeout(3000); // Flaky!
|
||||
|
||||
// ✅ Good: Wait for specific conditions
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForURL('/dashboard');
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.waitForURL("/dashboard");
|
||||
await page.waitForSelector('[data-testid="user-profile"]');
|
||||
|
||||
// ✅ Better: Auto-waiting with assertions
|
||||
await expect(page.getByText('Welcome')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Submit' }))
|
||||
.toBeEnabled();
|
||||
await expect(page.getByText("Welcome")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Submit" })).toBeEnabled();
|
||||
|
||||
// Wait for API response
|
||||
const responsePromise = page.waitForResponse(
|
||||
response => response.url().includes('/api/users') && response.status() === 200
|
||||
(response) =>
|
||||
response.url().includes("/api/users") && response.status() === 200,
|
||||
);
|
||||
await page.getByRole('button', { name: 'Load Users' }).click();
|
||||
await page.getByRole("button", { name: "Load Users" }).click();
|
||||
const response = await responsePromise;
|
||||
const data = await response.json();
|
||||
expect(data.users).toHaveLength(10);
|
||||
|
||||
// Wait for multiple conditions
|
||||
await Promise.all([
|
||||
page.waitForURL('/success'),
|
||||
page.waitForLoadState('networkidle'),
|
||||
expect(page.getByText('Payment successful')).toBeVisible(),
|
||||
page.waitForURL("/success"),
|
||||
page.waitForLoadState("networkidle"),
|
||||
expect(page.getByText("Payment successful")).toBeVisible(),
|
||||
]);
|
||||
```
|
||||
|
||||
@@ -246,49 +246,49 @@ await Promise.all([
|
||||
|
||||
```typescript
|
||||
// Mock API responses
|
||||
test('displays error when API fails', async ({ page }) => {
|
||||
await page.route('**/api/users', route => {
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ error: 'Internal Server Error' }),
|
||||
});
|
||||
test("displays error when API fails", async ({ page }) => {
|
||||
await page.route("**/api/users", (route) => {
|
||||
route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "Internal Server Error" }),
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/users');
|
||||
await expect(page.getByText('Failed to load users')).toBeVisible();
|
||||
await page.goto("/users");
|
||||
await expect(page.getByText("Failed to load users")).toBeVisible();
|
||||
});
|
||||
|
||||
// Intercept and modify requests
|
||||
test('can modify API request', async ({ page }) => {
|
||||
await page.route('**/api/users', async route => {
|
||||
const request = route.request();
|
||||
const postData = JSON.parse(request.postData() || '{}');
|
||||
test("can modify API request", async ({ page }) => {
|
||||
await page.route("**/api/users", async (route) => {
|
||||
const request = route.request();
|
||||
const postData = JSON.parse(request.postData() || "{}");
|
||||
|
||||
// Modify request
|
||||
postData.role = 'admin';
|
||||
// Modify request
|
||||
postData.role = "admin";
|
||||
|
||||
await route.continue({
|
||||
postData: JSON.stringify(postData),
|
||||
});
|
||||
await route.continue({
|
||||
postData: JSON.stringify(postData),
|
||||
});
|
||||
});
|
||||
|
||||
// Test continues...
|
||||
// Test continues...
|
||||
});
|
||||
|
||||
// Mock third-party services
|
||||
test('payment flow with mocked Stripe', async ({ page }) => {
|
||||
await page.route('**/api/stripe/**', route => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
id: 'mock_payment_id',
|
||||
status: 'succeeded',
|
||||
}),
|
||||
});
|
||||
test("payment flow with mocked Stripe", async ({ page }) => {
|
||||
await page.route("**/api/stripe/**", (route) => {
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
id: "mock_payment_id",
|
||||
status: "succeeded",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
// Test payment flow with mocked response
|
||||
// Test payment flow with mocked response
|
||||
});
|
||||
```
|
||||
|
||||
@@ -298,21 +298,21 @@ test('payment flow with mocked Stripe', async ({ page }) => {
|
||||
|
||||
```typescript
|
||||
// cypress.config.ts
|
||||
import { defineConfig } from 'cypress';
|
||||
import { defineConfig } from "cypress";
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
video: false,
|
||||
screenshotOnRunFailure: true,
|
||||
defaultCommandTimeout: 10000,
|
||||
requestTimeout: 10000,
|
||||
setupNodeEvents(on, config) {
|
||||
// Implement node event listeners
|
||||
},
|
||||
e2e: {
|
||||
baseUrl: "http://localhost:3000",
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
video: false,
|
||||
screenshotOnRunFailure: true,
|
||||
defaultCommandTimeout: 10000,
|
||||
requestTimeout: 10000,
|
||||
setupNodeEvents(on, config) {
|
||||
// Implement node event listeners
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@@ -321,68 +321,67 @@ export default defineConfig({
|
||||
```typescript
|
||||
// cypress/support/commands.ts
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
login(email: string, password: string): Chainable<void>;
|
||||
createUser(userData: UserData): Chainable<User>;
|
||||
dataCy(value: string): Chainable<JQuery<HTMLElement>>;
|
||||
}
|
||||
namespace Cypress {
|
||||
interface Chainable {
|
||||
login(email: string, password: string): Chainable<void>;
|
||||
createUser(userData: UserData): Chainable<User>;
|
||||
dataCy(value: string): Chainable<JQuery<HTMLElement>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cypress.Commands.add('login', (email: string, password: string) => {
|
||||
cy.visit('/login');
|
||||
cy.get('[data-testid="email"]').type(email);
|
||||
cy.get('[data-testid="password"]').type(password);
|
||||
cy.get('[data-testid="login-button"]').click();
|
||||
cy.url().should('include', '/dashboard');
|
||||
Cypress.Commands.add("login", (email: string, password: string) => {
|
||||
cy.visit("/login");
|
||||
cy.get('[data-testid="email"]').type(email);
|
||||
cy.get('[data-testid="password"]').type(password);
|
||||
cy.get('[data-testid="login-button"]').click();
|
||||
cy.url().should("include", "/dashboard");
|
||||
});
|
||||
|
||||
Cypress.Commands.add('createUser', (userData: UserData) => {
|
||||
return cy.request('POST', '/api/users', userData)
|
||||
.its('body');
|
||||
Cypress.Commands.add("createUser", (userData: UserData) => {
|
||||
return cy.request("POST", "/api/users", userData).its("body");
|
||||
});
|
||||
|
||||
Cypress.Commands.add('dataCy', (value: string) => {
|
||||
return cy.get(`[data-cy="${value}"]`);
|
||||
Cypress.Commands.add("dataCy", (value: string) => {
|
||||
return cy.get(`[data-cy="${value}"]`);
|
||||
});
|
||||
|
||||
// Usage
|
||||
cy.login('user@example.com', 'password');
|
||||
cy.dataCy('submit-button').click();
|
||||
cy.login("user@example.com", "password");
|
||||
cy.dataCy("submit-button").click();
|
||||
```
|
||||
|
||||
### Pattern 2: Cypress Intercept
|
||||
|
||||
```typescript
|
||||
// Mock API calls
|
||||
cy.intercept('GET', '/api/users', {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{ id: 1, name: 'John' },
|
||||
{ id: 2, name: 'Jane' },
|
||||
],
|
||||
}).as('getUsers');
|
||||
cy.intercept("GET", "/api/users", {
|
||||
statusCode: 200,
|
||||
body: [
|
||||
{ id: 1, name: "John" },
|
||||
{ id: 2, name: "Jane" },
|
||||
],
|
||||
}).as("getUsers");
|
||||
|
||||
cy.visit('/users');
|
||||
cy.wait('@getUsers');
|
||||
cy.get('[data-testid="user-list"]').children().should('have.length', 2);
|
||||
cy.visit("/users");
|
||||
cy.wait("@getUsers");
|
||||
cy.get('[data-testid="user-list"]').children().should("have.length", 2);
|
||||
|
||||
// Modify responses
|
||||
cy.intercept('GET', '/api/users', (req) => {
|
||||
req.reply((res) => {
|
||||
// Modify response
|
||||
res.body.users = res.body.users.slice(0, 5);
|
||||
res.send();
|
||||
});
|
||||
cy.intercept("GET", "/api/users", (req) => {
|
||||
req.reply((res) => {
|
||||
// Modify response
|
||||
res.body.users = res.body.users.slice(0, 5);
|
||||
res.send();
|
||||
});
|
||||
});
|
||||
|
||||
// Simulate slow network
|
||||
cy.intercept('GET', '/api/data', (req) => {
|
||||
req.reply((res) => {
|
||||
res.delay(3000); // 3 second delay
|
||||
res.send();
|
||||
});
|
||||
cy.intercept("GET", "/api/data", (req) => {
|
||||
req.reply((res) => {
|
||||
res.delay(3000); // 3 second delay
|
||||
res.send();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
@@ -392,31 +391,31 @@ cy.intercept('GET', '/api/data', (req) => {
|
||||
|
||||
```typescript
|
||||
// With Playwright
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test('homepage looks correct', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await expect(page).toHaveScreenshot('homepage.png', {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 100,
|
||||
});
|
||||
test("homepage looks correct", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page).toHaveScreenshot("homepage.png", {
|
||||
fullPage: true,
|
||||
maxDiffPixels: 100,
|
||||
});
|
||||
});
|
||||
|
||||
test('button in all states', async ({ page }) => {
|
||||
await page.goto('/components');
|
||||
test("button in all states", async ({ page }) => {
|
||||
await page.goto("/components");
|
||||
|
||||
const button = page.getByRole('button', { name: 'Submit' });
|
||||
const button = page.getByRole("button", { name: "Submit" });
|
||||
|
||||
// Default state
|
||||
await expect(button).toHaveScreenshot('button-default.png');
|
||||
// Default state
|
||||
await expect(button).toHaveScreenshot("button-default.png");
|
||||
|
||||
// Hover state
|
||||
await button.hover();
|
||||
await expect(button).toHaveScreenshot('button-hover.png');
|
||||
// Hover state
|
||||
await button.hover();
|
||||
await expect(button).toHaveScreenshot("button-hover.png");
|
||||
|
||||
// Disabled state
|
||||
await button.evaluate(el => el.setAttribute('disabled', 'true'));
|
||||
await expect(button).toHaveScreenshot('button-disabled.png');
|
||||
// Disabled state
|
||||
await button.evaluate((el) => el.setAttribute("disabled", "true"));
|
||||
await expect(button).toHaveScreenshot("button-disabled.png");
|
||||
});
|
||||
```
|
||||
|
||||
@@ -425,20 +424,20 @@ test('button in all states', async ({ page }) => {
|
||||
```typescript
|
||||
// playwright.config.ts
|
||||
export default defineConfig({
|
||||
projects: [
|
||||
{
|
||||
name: 'shard-1',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
grepInvert: /@slow/,
|
||||
shard: { current: 1, total: 4 },
|
||||
},
|
||||
{
|
||||
name: 'shard-2',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
shard: { current: 2, total: 4 },
|
||||
},
|
||||
// ... more shards
|
||||
],
|
||||
projects: [
|
||||
{
|
||||
name: "shard-1",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
grepInvert: /@slow/,
|
||||
shard: { current: 1, total: 4 },
|
||||
},
|
||||
{
|
||||
name: "shard-2",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
shard: { current: 2, total: 4 },
|
||||
},
|
||||
// ... more shards
|
||||
],
|
||||
});
|
||||
|
||||
// Run in CI
|
||||
@@ -450,27 +449,25 @@ export default defineConfig({
|
||||
|
||||
```typescript
|
||||
// Install: npm install @axe-core/playwright
|
||||
import { test, expect } from '@playwright/test';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { test, expect } from "@playwright/test";
|
||||
import AxeBuilder from "@axe-core/playwright";
|
||||
|
||||
test('page should not have accessibility violations', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
test("page should not have accessibility violations", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||
.exclude('#third-party-widget')
|
||||
.analyze();
|
||||
const accessibilityScanResults = await new AxeBuilder({ page })
|
||||
.exclude("#third-party-widget")
|
||||
.analyze();
|
||||
|
||||
expect(accessibilityScanResults.violations).toEqual([]);
|
||||
expect(accessibilityScanResults.violations).toEqual([]);
|
||||
});
|
||||
|
||||
test('form is accessible', async ({ page }) => {
|
||||
await page.goto('/signup');
|
||||
test("form is accessible", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.include('form')
|
||||
.analyze();
|
||||
const results = await new AxeBuilder({ page }).include("form").analyze();
|
||||
|
||||
expect(results.violations).toEqual([]);
|
||||
expect(results.violations).toEqual([]);
|
||||
});
|
||||
```
|
||||
|
||||
@@ -487,13 +484,13 @@ test('form is accessible', async ({ page }) => {
|
||||
|
||||
```typescript
|
||||
// ❌ Bad selectors
|
||||
cy.get('.btn.btn-primary.submit-button').click();
|
||||
cy.get('div > form > div:nth-child(2) > input').type('text');
|
||||
cy.get(".btn.btn-primary.submit-button").click();
|
||||
cy.get("div > form > div:nth-child(2) > input").type("text");
|
||||
|
||||
// ✅ Good selectors
|
||||
cy.getByRole('button', { name: 'Submit' }).click();
|
||||
cy.getByLabel('Email address').type('user@example.com');
|
||||
cy.get('[data-testid="email-input"]').type('user@example.com');
|
||||
cy.getByRole("button", { name: "Submit" }).click();
|
||||
cy.getByLabel("Email address").type("user@example.com");
|
||||
cy.get('[data-testid="email-input"]').type("user@example.com");
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
@@ -23,12 +23,14 @@ Build resilient applications with robust error handling strategies that graceful
|
||||
### 1. Error Handling Philosophies
|
||||
|
||||
**Exceptions vs Result Types:**
|
||||
|
||||
- **Exceptions**: Traditional try-catch, disrupts control flow
|
||||
- **Result Types**: Explicit success/failure, functional approach
|
||||
- **Error Codes**: C-style, requires discipline
|
||||
- **Option/Maybe Types**: For nullable values
|
||||
|
||||
**When to Use Each:**
|
||||
|
||||
- Exceptions: Unexpected errors, exceptional conditions
|
||||
- Result Types: Expected errors, validation failures
|
||||
- Panics/Crashes: Unrecoverable errors, programming bugs
|
||||
@@ -36,12 +38,14 @@ Build resilient applications with robust error handling strategies that graceful
|
||||
### 2. Error Categories
|
||||
|
||||
**Recoverable Errors:**
|
||||
|
||||
- Network timeouts
|
||||
- Missing files
|
||||
- Invalid user input
|
||||
- API rate limits
|
||||
|
||||
**Unrecoverable Errors:**
|
||||
|
||||
- Out of memory
|
||||
- Stack overflow
|
||||
- Programming bugs (null pointer, etc.)
|
||||
@@ -51,6 +55,7 @@ Build resilient applications with robust error handling strategies that graceful
|
||||
### Python Error Handling
|
||||
|
||||
**Custom Exception Hierarchy:**
|
||||
|
||||
```python
|
||||
class ApplicationError(Exception):
|
||||
"""Base exception for all application errors."""
|
||||
@@ -87,6 +92,7 @@ def get_user(user_id: str) -> User:
|
||||
```
|
||||
|
||||
**Context Managers for Cleanup:**
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
|
||||
@@ -110,6 +116,7 @@ with database_transaction(db.session) as session:
|
||||
```
|
||||
|
||||
**Retry with Exponential Backoff:**
|
||||
|
||||
```python
|
||||
import time
|
||||
from functools import wraps
|
||||
@@ -152,131 +159,128 @@ def fetch_data(url: str) -> dict:
|
||||
### TypeScript/JavaScript Error Handling
|
||||
|
||||
**Custom Error Classes:**
|
||||
|
||||
```typescript
|
||||
// Custom error classes
|
||||
class ApplicationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public statusCode: number = 500,
|
||||
public details?: Record<string, any>
|
||||
) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public statusCode: number = 500,
|
||||
public details?: Record<string, any>,
|
||||
) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends ApplicationError {
|
||||
constructor(message: string, details?: Record<string, any>) {
|
||||
super(message, 'VALIDATION_ERROR', 400, details);
|
||||
}
|
||||
constructor(message: string, details?: Record<string, any>) {
|
||||
super(message, "VALIDATION_ERROR", 400, details);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends ApplicationError {
|
||||
constructor(resource: string, id: string) {
|
||||
super(
|
||||
`${resource} not found`,
|
||||
'NOT_FOUND',
|
||||
404,
|
||||
{ resource, id }
|
||||
);
|
||||
}
|
||||
constructor(resource: string, id: string) {
|
||||
super(`${resource} not found`, "NOT_FOUND", 404, { resource, id });
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
function getUser(id: string): User {
|
||||
const user = users.find(u => u.id === id);
|
||||
if (!user) {
|
||||
throw new NotFoundError('User', id);
|
||||
}
|
||||
return user;
|
||||
const user = users.find((u) => u.id === id);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User", id);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
**Result Type Pattern:**
|
||||
|
||||
```typescript
|
||||
// Result type for explicit error handling
|
||||
type Result<T, E = Error> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: E };
|
||||
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
||||
|
||||
// Helper functions
|
||||
function Ok<T>(value: T): Result<T, never> {
|
||||
return { ok: true, value };
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
function Err<E>(error: E): Result<never, E> {
|
||||
return { ok: false, error };
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
// Usage
|
||||
function parseJSON<T>(json: string): Result<T, SyntaxError> {
|
||||
try {
|
||||
const value = JSON.parse(json) as T;
|
||||
return Ok(value);
|
||||
} catch (error) {
|
||||
return Err(error as SyntaxError);
|
||||
}
|
||||
try {
|
||||
const value = JSON.parse(json) as T;
|
||||
return Ok(value);
|
||||
} catch (error) {
|
||||
return Err(error as SyntaxError);
|
||||
}
|
||||
}
|
||||
|
||||
// Consuming Result
|
||||
const result = parseJSON<User>(userJson);
|
||||
if (result.ok) {
|
||||
console.log(result.value.name);
|
||||
console.log(result.value.name);
|
||||
} else {
|
||||
console.error('Parse failed:', result.error.message);
|
||||
console.error("Parse failed:", result.error.message);
|
||||
}
|
||||
|
||||
// Chaining Results
|
||||
function chain<T, U, E>(
|
||||
result: Result<T, E>,
|
||||
fn: (value: T) => Result<U, E>
|
||||
result: Result<T, E>,
|
||||
fn: (value: T) => Result<U, E>,
|
||||
): Result<U, E> {
|
||||
return result.ok ? fn(result.value) : result;
|
||||
return result.ok ? fn(result.value) : result;
|
||||
}
|
||||
```
|
||||
|
||||
**Async Error Handling:**
|
||||
|
||||
```typescript
|
||||
// Async/await with proper error handling
|
||||
async function fetchUserOrders(userId: string): Promise<Order[]> {
|
||||
try {
|
||||
const user = await getUser(userId);
|
||||
const orders = await getOrders(user.id);
|
||||
return orders;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
return []; // Return empty array for not found
|
||||
}
|
||||
if (error instanceof NetworkError) {
|
||||
// Retry logic
|
||||
return retryFetchOrders(userId);
|
||||
}
|
||||
// Re-throw unexpected errors
|
||||
throw error;
|
||||
try {
|
||||
const user = await getUser(userId);
|
||||
const orders = await getOrders(user.id);
|
||||
return orders;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
return []; // Return empty array for not found
|
||||
}
|
||||
if (error instanceof NetworkError) {
|
||||
// Retry logic
|
||||
return retryFetchOrders(userId);
|
||||
}
|
||||
// Re-throw unexpected errors
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Promise error handling
|
||||
function fetchData(url: string): Promise<Data> {
|
||||
return fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new NetworkError(`HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Fetch failed:', error);
|
||||
throw error;
|
||||
});
|
||||
return fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new NetworkError(`HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Fetch failed:", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Rust Error Handling
|
||||
|
||||
**Result and Option Types:**
|
||||
|
||||
```rust
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
@@ -328,6 +332,7 @@ fn get_user_age(id: &str) -> Result<u32, AppError> {
|
||||
### Go Error Handling
|
||||
|
||||
**Explicit Error Returns:**
|
||||
|
||||
```go
|
||||
// Basic error handling
|
||||
func getUser(id string) (*User, error) {
|
||||
@@ -464,54 +469,54 @@ Collect multiple errors instead of failing on first error.
|
||||
|
||||
```typescript
|
||||
class ErrorCollector {
|
||||
private errors: Error[] = [];
|
||||
private errors: Error[] = [];
|
||||
|
||||
add(error: Error): void {
|
||||
this.errors.push(error);
|
||||
}
|
||||
add(error: Error): void {
|
||||
this.errors.push(error);
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
return this.errors.length > 0;
|
||||
}
|
||||
hasErrors(): boolean {
|
||||
return this.errors.length > 0;
|
||||
}
|
||||
|
||||
getErrors(): Error[] {
|
||||
return [...this.errors];
|
||||
}
|
||||
getErrors(): Error[] {
|
||||
return [...this.errors];
|
||||
}
|
||||
|
||||
throw(): never {
|
||||
if (this.errors.length === 1) {
|
||||
throw this.errors[0];
|
||||
}
|
||||
throw new AggregateError(
|
||||
this.errors,
|
||||
`${this.errors.length} errors occurred`
|
||||
);
|
||||
throw(): never {
|
||||
if (this.errors.length === 1) {
|
||||
throw this.errors[0];
|
||||
}
|
||||
throw new AggregateError(
|
||||
this.errors,
|
||||
`${this.errors.length} errors occurred`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: Validate multiple fields
|
||||
function validateUser(data: any): User {
|
||||
const errors = new ErrorCollector();
|
||||
const errors = new ErrorCollector();
|
||||
|
||||
if (!data.email) {
|
||||
errors.add(new ValidationError('Email is required'));
|
||||
} else if (!isValidEmail(data.email)) {
|
||||
errors.add(new ValidationError('Email is invalid'));
|
||||
}
|
||||
if (!data.email) {
|
||||
errors.add(new ValidationError("Email is required"));
|
||||
} else if (!isValidEmail(data.email)) {
|
||||
errors.add(new ValidationError("Email is invalid"));
|
||||
}
|
||||
|
||||
if (!data.name || data.name.length < 2) {
|
||||
errors.add(new ValidationError('Name must be at least 2 characters'));
|
||||
}
|
||||
if (!data.name || data.name.length < 2) {
|
||||
errors.add(new ValidationError("Name must be at least 2 characters"));
|
||||
}
|
||||
|
||||
if (!data.age || data.age < 18) {
|
||||
errors.add(new ValidationError('Age must be 18 or older'));
|
||||
}
|
||||
if (!data.age || data.age < 18) {
|
||||
errors.add(new ValidationError("Age must be 18 or older"));
|
||||
}
|
||||
|
||||
if (errors.hasErrors()) {
|
||||
errors.throw();
|
||||
}
|
||||
if (errors.hasErrors()) {
|
||||
errors.throw();
|
||||
}
|
||||
|
||||
return data as User;
|
||||
return data as User;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ Master advanced Git techniques to maintain clean history, collaborate effectivel
|
||||
Interactive rebase is the Swiss Army knife of Git history editing.
|
||||
|
||||
**Common Operations:**
|
||||
|
||||
- `pick`: Keep commit as-is
|
||||
- `reword`: Change commit message
|
||||
- `edit`: Amend commit content
|
||||
@@ -33,6 +34,7 @@ Interactive rebase is the Swiss Army knife of Git history editing.
|
||||
- `drop`: Remove commit entirely
|
||||
|
||||
**Basic Usage:**
|
||||
|
||||
```bash
|
||||
# Rebase last 5 commits
|
||||
git rebase -i HEAD~5
|
||||
@@ -86,6 +88,7 @@ git bisect reset
|
||||
```
|
||||
|
||||
**Automated Bisect:**
|
||||
|
||||
```bash
|
||||
# Use script to test automatically
|
||||
git bisect start HEAD v1.0.0
|
||||
@@ -251,11 +254,13 @@ git branch recovery def456
|
||||
### Rebase vs Merge Strategy
|
||||
|
||||
**When to Rebase:**
|
||||
|
||||
- Cleaning up local commits before pushing
|
||||
- Keeping feature branch up-to-date with main
|
||||
- Creating linear history for easier review
|
||||
|
||||
**When to Merge:**
|
||||
|
||||
- Integrating completed features into main
|
||||
- Preserving exact history of collaboration
|
||||
- Public branches used by others
|
||||
|
||||
@@ -23,6 +23,7 @@ Build efficient, scalable monorepos that enable code sharing, consistent tooling
|
||||
### 1. Why Monorepos?
|
||||
|
||||
**Advantages:**
|
||||
|
||||
- Shared code and dependencies
|
||||
- Atomic commits across projects
|
||||
- Consistent tooling and standards
|
||||
@@ -31,6 +32,7 @@ Build efficient, scalable monorepos that enable code sharing, consistent tooling
|
||||
- Better code visibility
|
||||
|
||||
**Challenges:**
|
||||
|
||||
- Build performance at scale
|
||||
- CI/CD complexity
|
||||
- Access control
|
||||
@@ -39,11 +41,13 @@ Build efficient, scalable monorepos that enable code sharing, consistent tooling
|
||||
### 2. Monorepo Tools
|
||||
|
||||
**Package Managers:**
|
||||
|
||||
- pnpm workspaces (recommended)
|
||||
- npm workspaces
|
||||
- Yarn workspaces
|
||||
|
||||
**Build Systems:**
|
||||
|
||||
- Turborepo (recommended for most)
|
||||
- Nx (feature-rich, complex)
|
||||
- Lerna (older, maintenance mode)
|
||||
@@ -105,10 +109,7 @@ cd my-monorepo
|
||||
{
|
||||
"name": "my-monorepo",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"workspaces": ["apps/*", "packages/*"],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
@@ -170,9 +171,9 @@ cd my-monorepo
|
||||
```yaml
|
||||
# pnpm-workspace.yaml
|
||||
packages:
|
||||
- 'apps/*'
|
||||
- 'packages/*'
|
||||
- 'tools/*'
|
||||
- "apps/*"
|
||||
- "packages/*"
|
||||
- "tools/*"
|
||||
```
|
||||
|
||||
```json
|
||||
@@ -346,35 +347,35 @@ nx run-many --target=build --all --parallel=3
|
||||
// packages/config/eslint-preset.js
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'prettier',
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier",
|
||||
],
|
||||
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ["@typescript-eslint", "react", "react-hooks"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
version: "detect",
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-unused-vars': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
};
|
||||
|
||||
// apps/web/.eslintrc.js
|
||||
module.exports = {
|
||||
extends: ['@repo/config/eslint-preset'],
|
||||
extends: ["@repo/config/eslint-preset"],
|
||||
rules: {
|
||||
// App-specific rules
|
||||
},
|
||||
@@ -427,16 +428,16 @@ export function capitalize(str: string): string {
|
||||
}
|
||||
|
||||
export function truncate(str: string, length: number): string {
|
||||
return str.length > length ? str.slice(0, length) + '...' : str;
|
||||
return str.length > length ? str.slice(0, length) + "..." : str;
|
||||
}
|
||||
|
||||
// packages/utils/src/index.ts
|
||||
export * from './string';
|
||||
export * from './array';
|
||||
export * from './date';
|
||||
export * from "./string";
|
||||
export * from "./array";
|
||||
export * from "./date";
|
||||
|
||||
// Usage in apps
|
||||
import { capitalize, truncate } from '@repo/utils';
|
||||
import { capitalize, truncate } from "@repo/utils";
|
||||
```
|
||||
|
||||
### Pattern 3: Shared Types
|
||||
@@ -447,7 +448,7 @@ export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: 'admin' | 'user';
|
||||
role: "admin" | "user";
|
||||
}
|
||||
|
||||
export interface CreateUserInput {
|
||||
@@ -457,7 +458,7 @@ export interface CreateUserInput {
|
||||
}
|
||||
|
||||
// Used in both frontend and backend
|
||||
import type { User, CreateUserInput } from '@repo/types';
|
||||
import type { User, CreateUserInput } from "@repo/types";
|
||||
```
|
||||
|
||||
## Build Optimization
|
||||
@@ -525,7 +526,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # For Nx affected commands
|
||||
fetch-depth: 0 # For Nx affected commands
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
@@ -534,7 +535,7 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'pnpm'
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
@@ -39,13 +39,13 @@ workspace/
|
||||
|
||||
### 2. Library Types
|
||||
|
||||
| Type | Purpose | Example |
|
||||
|------|---------|---------|
|
||||
| **feature** | Smart components, business logic | `feature-auth` |
|
||||
| **ui** | Presentational components | `ui-buttons` |
|
||||
| **data-access** | API calls, state management | `data-access-users` |
|
||||
| **util** | Pure functions, helpers | `util-formatting` |
|
||||
| **shell** | App bootstrapping | `shell-web` |
|
||||
| Type | Purpose | Example |
|
||||
| --------------- | -------------------------------- | ------------------- |
|
||||
| **feature** | Smart components, business logic | `feature-auth` |
|
||||
| **ui** | Presentational components | `ui-buttons` |
|
||||
| **data-access** | API calls, state management | `data-access-users` |
|
||||
| **util** | Pure functions, helpers | `util-formatting` |
|
||||
| **shell** | App bootstrapping | `shell-web` |
|
||||
|
||||
## Templates
|
||||
|
||||
@@ -276,8 +276,8 @@ import {
|
||||
joinPathFragments,
|
||||
names,
|
||||
readProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { libraryGenerator } from '@nx/react';
|
||||
} from "@nx/devkit";
|
||||
import { libraryGenerator } from "@nx/react";
|
||||
|
||||
interface FeatureLibraryGeneratorSchema {
|
||||
name: string;
|
||||
@@ -287,7 +287,7 @@ interface FeatureLibraryGeneratorSchema {
|
||||
|
||||
export default async function featureLibraryGenerator(
|
||||
tree: Tree,
|
||||
options: FeatureLibraryGeneratorSchema
|
||||
options: FeatureLibraryGeneratorSchema,
|
||||
) {
|
||||
const { name, scope, directory } = options;
|
||||
const projectDirectory = directory
|
||||
@@ -299,26 +299,29 @@ export default async function featureLibraryGenerator(
|
||||
name: `feature-${name}`,
|
||||
directory: projectDirectory,
|
||||
tags: `type:feature,scope:${scope}`,
|
||||
style: 'css',
|
||||
style: "css",
|
||||
skipTsConfig: false,
|
||||
skipFormat: true,
|
||||
unitTestRunner: 'jest',
|
||||
linter: 'eslint',
|
||||
unitTestRunner: "jest",
|
||||
linter: "eslint",
|
||||
});
|
||||
|
||||
// Add custom files
|
||||
const projectConfig = readProjectConfiguration(tree, `${scope}-feature-${name}`);
|
||||
const projectConfig = readProjectConfiguration(
|
||||
tree,
|
||||
`${scope}-feature-${name}`,
|
||||
);
|
||||
const projectNames = names(name);
|
||||
|
||||
generateFiles(
|
||||
tree,
|
||||
joinPathFragments(__dirname, 'files'),
|
||||
joinPathFragments(__dirname, "files"),
|
||||
projectConfig.sourceRoot,
|
||||
{
|
||||
...projectNames,
|
||||
scope,
|
||||
tmpl: '',
|
||||
}
|
||||
tmpl: "",
|
||||
},
|
||||
);
|
||||
|
||||
await formatFiles(tree);
|
||||
@@ -351,7 +354,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -433,6 +436,7 @@ nx migrate --run-migrations
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- **Use tags consistently** - Enforce with module boundaries
|
||||
- **Enable caching early** - Significant CI savings
|
||||
- **Keep libs focused** - Single responsibility
|
||||
@@ -440,6 +444,7 @@ nx migrate --run-migrations
|
||||
- **Document boundaries** - Help new developers
|
||||
|
||||
### Don'ts
|
||||
|
||||
- **Don't create circular deps** - Graph should be acyclic
|
||||
- **Don't skip affected** - Test only what changed
|
||||
- **Don't ignore boundaries** - Tech debt accumulates
|
||||
|
||||
@@ -25,6 +25,7 @@ Transform slow database queries into lightning-fast operations through systemati
|
||||
Understanding EXPLAIN output is fundamental to optimization.
|
||||
|
||||
**PostgreSQL EXPLAIN:**
|
||||
|
||||
```sql
|
||||
-- Basic explain
|
||||
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
|
||||
@@ -42,6 +43,7 @@ WHERE u.created_at > NOW() - INTERVAL '30 days';
|
||||
```
|
||||
|
||||
**Key Metrics to Watch:**
|
||||
|
||||
- **Seq Scan**: Full table scan (usually slow for large tables)
|
||||
- **Index Scan**: Using index (good)
|
||||
- **Index Only Scan**: Using index without touching table (best)
|
||||
@@ -57,6 +59,7 @@ WHERE u.created_at > NOW() - INTERVAL '30 days';
|
||||
Indexes are the most powerful optimization tool.
|
||||
|
||||
**Index Types:**
|
||||
|
||||
- **B-Tree**: Default, good for equality and range queries
|
||||
- **Hash**: Only for equality (=) comparisons
|
||||
- **GIN**: Full-text search, array queries, JSONB
|
||||
@@ -92,6 +95,7 @@ CREATE INDEX idx_metadata ON events USING GIN(metadata);
|
||||
### 3. Query Optimization Patterns
|
||||
|
||||
**Avoid SELECT \*:**
|
||||
|
||||
```sql
|
||||
-- Bad: Fetches unnecessary columns
|
||||
SELECT * FROM users WHERE id = 123;
|
||||
@@ -101,6 +105,7 @@ SELECT id, email, name FROM users WHERE id = 123;
|
||||
```
|
||||
|
||||
**Use WHERE Clause Efficiently:**
|
||||
|
||||
```sql
|
||||
-- Bad: Function prevents index usage
|
||||
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
|
||||
@@ -115,6 +120,7 @@ SELECT * FROM users WHERE email = 'user@example.com';
|
||||
```
|
||||
|
||||
**Optimize JOINs:**
|
||||
|
||||
```sql
|
||||
-- Bad: Cartesian product then filter
|
||||
SELECT u.name, o.total
|
||||
@@ -138,6 +144,7 @@ JOIN orders o ON u.id = o.user_id;
|
||||
### Pattern 1: Eliminate N+1 Queries
|
||||
|
||||
**Problem: N+1 Query Anti-Pattern**
|
||||
|
||||
```python
|
||||
# Bad: Executes N+1 queries
|
||||
users = db.query("SELECT * FROM users LIMIT 10")
|
||||
@@ -147,6 +154,7 @@ for user in users:
|
||||
```
|
||||
|
||||
**Solution: Use JOINs or Batch Loading**
|
||||
|
||||
```sql
|
||||
-- Solution 1: JOIN
|
||||
SELECT
|
||||
@@ -187,6 +195,7 @@ for order in orders:
|
||||
### Pattern 2: Optimize Pagination
|
||||
|
||||
**Bad: OFFSET on Large Tables**
|
||||
|
||||
```sql
|
||||
-- Slow for large offsets
|
||||
SELECT * FROM users
|
||||
@@ -195,6 +204,7 @@ LIMIT 20 OFFSET 100000; -- Very slow!
|
||||
```
|
||||
|
||||
**Good: Cursor-Based Pagination**
|
||||
|
||||
```sql
|
||||
-- Much faster: Use cursor (last seen ID)
|
||||
SELECT * FROM users
|
||||
@@ -215,6 +225,7 @@ CREATE INDEX idx_users_cursor ON users(created_at DESC, id DESC);
|
||||
### Pattern 3: Aggregate Efficiently
|
||||
|
||||
**Optimize COUNT Queries:**
|
||||
|
||||
```sql
|
||||
-- Bad: Counts all rows
|
||||
SELECT COUNT(*) FROM orders; -- Slow on large tables
|
||||
@@ -235,6 +246,7 @@ WHERE created_at > NOW() - INTERVAL '7 days';
|
||||
```
|
||||
|
||||
**Optimize GROUP BY:**
|
||||
|
||||
```sql
|
||||
-- Bad: Group by then filter
|
||||
SELECT user_id, COUNT(*) as order_count
|
||||
@@ -256,6 +268,7 @@ CREATE INDEX idx_orders_user_status ON orders(user_id, status);
|
||||
### Pattern 4: Subquery Optimization
|
||||
|
||||
**Transform Correlated Subqueries:**
|
||||
|
||||
```sql
|
||||
-- Bad: Correlated subquery (runs for each row)
|
||||
SELECT u.name, u.email,
|
||||
@@ -277,6 +290,7 @@ LEFT JOIN orders o ON o.user_id = u.id;
|
||||
```
|
||||
|
||||
**Use CTEs for Clarity:**
|
||||
|
||||
```sql
|
||||
-- Using Common Table Expressions
|
||||
WITH recent_users AS (
|
||||
@@ -298,6 +312,7 @@ LEFT JOIN user_order_counts uoc ON ru.id = uoc.user_id;
|
||||
### Pattern 5: Batch Operations
|
||||
|
||||
**Batch INSERT:**
|
||||
|
||||
```sql
|
||||
-- Bad: Multiple individual inserts
|
||||
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
|
||||
@@ -315,6 +330,7 @@ COPY users (name, email) FROM '/tmp/users.csv' CSV HEADER;
|
||||
```
|
||||
|
||||
**Batch UPDATE:**
|
||||
|
||||
```sql
|
||||
-- Bad: Update in loop
|
||||
UPDATE users SET status = 'active' WHERE id = 1;
|
||||
|
||||
@@ -38,12 +38,12 @@ Workspace Root/
|
||||
|
||||
### 2. Pipeline Concepts
|
||||
|
||||
| Concept | Description |
|
||||
|---------|-------------|
|
||||
| **dependsOn** | Tasks that must complete first |
|
||||
| **cache** | Whether to cache outputs |
|
||||
| **outputs** | Files to cache |
|
||||
| **inputs** | Files that affect cache key |
|
||||
| Concept | Description |
|
||||
| -------------- | -------------------------------- |
|
||||
| **dependsOn** | Tasks that must complete first |
|
||||
| **cache** | Whether to cache outputs |
|
||||
| **outputs** | Files to cache |
|
||||
| **inputs** | Files that affect cache key |
|
||||
| **persistent** | Long-running tasks (dev servers) |
|
||||
|
||||
## Templates
|
||||
@@ -53,35 +53,18 @@ Workspace Root/
|
||||
```json
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"globalDependencies": [
|
||||
".env",
|
||||
".env.local"
|
||||
],
|
||||
"globalEnv": [
|
||||
"NODE_ENV",
|
||||
"VERCEL_URL"
|
||||
],
|
||||
"globalDependencies": [".env", ".env.local"],
|
||||
"globalEnv": ["NODE_ENV", "VERCEL_URL"],
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": [
|
||||
"dist/**",
|
||||
".next/**",
|
||||
"!.next/cache/**"
|
||||
],
|
||||
"env": [
|
||||
"API_URL",
|
||||
"NEXT_PUBLIC_*"
|
||||
]
|
||||
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
|
||||
"env": ["API_URL", "NEXT_PUBLIC_*"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["build"],
|
||||
"outputs": ["coverage/**"],
|
||||
"inputs": [
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
]
|
||||
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
|
||||
},
|
||||
"lint": {
|
||||
"outputs": [],
|
||||
@@ -112,18 +95,11 @@ Workspace Root/
|
||||
"pipeline": {
|
||||
"build": {
|
||||
"outputs": [".next/**", "!.next/cache/**"],
|
||||
"env": [
|
||||
"NEXT_PUBLIC_API_URL",
|
||||
"NEXT_PUBLIC_ANALYTICS_ID"
|
||||
]
|
||||
"env": ["NEXT_PUBLIC_API_URL", "NEXT_PUBLIC_ANALYTICS_ID"]
|
||||
},
|
||||
"test": {
|
||||
"outputs": ["coverage/**"],
|
||||
"inputs": [
|
||||
"src/**",
|
||||
"tests/**",
|
||||
"jest.config.js"
|
||||
]
|
||||
"inputs": ["src/**", "tests/**", "jest.config.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +144,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -184,32 +160,32 @@ jobs:
|
||||
|
||||
```typescript
|
||||
// Custom remote cache server (Express)
|
||||
import express from 'express';
|
||||
import { createReadStream, createWriteStream } from 'fs';
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import express from "express";
|
||||
import { createReadStream, createWriteStream } from "fs";
|
||||
import { mkdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
|
||||
const app = express();
|
||||
const CACHE_DIR = './cache';
|
||||
const CACHE_DIR = "./cache";
|
||||
|
||||
// Get artifact
|
||||
app.get('/v8/artifacts/:hash', async (req, res) => {
|
||||
app.get("/v8/artifacts/:hash", async (req, res) => {
|
||||
const { hash } = req.params;
|
||||
const team = req.query.teamId || 'default';
|
||||
const team = req.query.teamId || "default";
|
||||
const filePath = join(CACHE_DIR, team, hash);
|
||||
|
||||
try {
|
||||
const stream = createReadStream(filePath);
|
||||
stream.pipe(res);
|
||||
} catch {
|
||||
res.status(404).send('Not found');
|
||||
res.status(404).send("Not found");
|
||||
}
|
||||
});
|
||||
|
||||
// Put artifact
|
||||
app.put('/v8/artifacts/:hash', async (req, res) => {
|
||||
app.put("/v8/artifacts/:hash", async (req, res) => {
|
||||
const { hash } = req.params;
|
||||
const team = req.query.teamId || 'default';
|
||||
const team = req.query.teamId || "default";
|
||||
const dir = join(CACHE_DIR, team);
|
||||
const filePath = join(dir, hash);
|
||||
|
||||
@@ -218,15 +194,17 @@ app.put('/v8/artifacts/:hash', async (req, res) => {
|
||||
const stream = createWriteStream(filePath);
|
||||
req.pipe(stream);
|
||||
|
||||
stream.on('finish', () => {
|
||||
res.json({ urls: [`${req.protocol}://${req.get('host')}/v8/artifacts/${hash}`] });
|
||||
stream.on("finish", () => {
|
||||
res.json({
|
||||
urls: [`${req.protocol}://${req.get("host")}/v8/artifacts/${hash}`],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Check artifact exists
|
||||
app.head('/v8/artifacts/:hash', async (req, res) => {
|
||||
app.head("/v8/artifacts/:hash", async (req, res) => {
|
||||
const { hash } = req.params;
|
||||
const team = req.query.teamId || 'default';
|
||||
const team = req.query.teamId || "default";
|
||||
const filePath = join(CACHE_DIR, team, hash);
|
||||
|
||||
try {
|
||||
@@ -291,20 +269,12 @@ turbo build --filter='...[HEAD^1]...'
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["dist/**"],
|
||||
"inputs": [
|
||||
"$TURBO_DEFAULT$",
|
||||
"!**/*.md",
|
||||
"!**/*.test.*"
|
||||
]
|
||||
"inputs": ["$TURBO_DEFAULT$", "!**/*.md", "!**/*.test.*"]
|
||||
},
|
||||
"test": {
|
||||
"dependsOn": ["^build"],
|
||||
"outputs": ["coverage/**"],
|
||||
"inputs": [
|
||||
"src/**",
|
||||
"tests/**",
|
||||
"*.config.*"
|
||||
],
|
||||
"inputs": ["src/**", "tests/**", "*.config.*"],
|
||||
"env": ["CI", "NODE_ENV"]
|
||||
},
|
||||
"test:e2e": {
|
||||
@@ -339,10 +309,7 @@ turbo build --filter='...[HEAD^1]...'
|
||||
{
|
||||
"name": "my-turborepo",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"workspaces": ["apps/*", "packages/*"],
|
||||
"scripts": {
|
||||
"build": "turbo build",
|
||||
"dev": "turbo dev",
|
||||
@@ -388,6 +355,7 @@ TURBO_LOG_VERBOSITY=debug turbo build --filter=@myorg/web
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- **Define explicit inputs** - Avoid cache invalidation
|
||||
- **Use workspace protocol** - `"@myorg/ui": "workspace:*"`
|
||||
- **Enable remote caching** - Share across CI and local
|
||||
@@ -395,6 +363,7 @@ TURBO_LOG_VERBOSITY=debug turbo build --filter=@myorg/web
|
||||
- **Cache build outputs** - Not source files
|
||||
|
||||
### Don'ts
|
||||
|
||||
- **Don't cache dev servers** - Use `persistent: true`
|
||||
- **Don't include secrets in env** - Use runtime env vars
|
||||
- **Don't ignore dependsOn** - Causes race conditions
|
||||
|
||||
Reference in New Issue
Block a user