yantr-js 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,140 @@
1
+ import bcrypt from 'bcryptjs';
2
+ import {
3
+ generateAccessToken,
4
+ generateRefreshToken,
5
+ verifyToken,
6
+ type TokenPayload
7
+ } from './auth.middleware';
8
+
9
+ // Types
10
+ export interface RegisterInput {
11
+ email: string;
12
+ password: string;
13
+ name?: string;
14
+ }
15
+
16
+ export interface LoginInput {
17
+ email: string;
18
+ password: string;
19
+ }
20
+
21
+ export interface AuthTokens {
22
+ accessToken: string;
23
+ refreshToken: string;
24
+ }
25
+
26
+ export interface UserData {
27
+ id: string;
28
+ email: string;
29
+ name?: string;
30
+ role?: string;
31
+ }
32
+
33
+ // Constants
34
+ const SALT_ROUNDS = 12;
35
+
36
+ /**
37
+ * Hash a password
38
+ */
39
+ export async function hashPassword(password: string): Promise<string> {
40
+ return bcrypt.hash(password, SALT_ROUNDS);
41
+ }
42
+
43
+ /**
44
+ * Compare password with hash
45
+ */
46
+ export async function comparePassword(
47
+ password: string,
48
+ hash: string
49
+ ): Promise<boolean> {
50
+ return bcrypt.compare(password, hash);
51
+ }
52
+
53
+ /**
54
+ * Generate auth tokens for a user
55
+ */
56
+ export function generateAuthTokens(user: UserData): AuthTokens {
57
+ const payload: TokenPayload = {
58
+ userId: user.id,
59
+ email: user.email,
60
+ role: user.role,
61
+ };
62
+
63
+ return {
64
+ accessToken: generateAccessToken(payload),
65
+ refreshToken: generateRefreshToken(payload),
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Refresh access token using refresh token
71
+ */
72
+ export function refreshAccessToken(refreshToken: string): AuthTokens {
73
+ const payload = verifyToken(refreshToken);
74
+
75
+ return {
76
+ accessToken: generateAccessToken(payload),
77
+ refreshToken: generateRefreshToken(payload),
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Example: Register a new user
83
+ *
84
+ * NOTE: This is a template. You need to implement your own user storage logic.
85
+ * Replace the TODO comments with your database operations.
86
+ */
87
+ export async function registerUser(input: RegisterInput): Promise<UserData> {
88
+ const { email, password, name } = input;
89
+
90
+ // TODO: Check if user already exists in your database
91
+ // const existingUser = await prisma.user.findUnique({ where: { email } });
92
+ // if (existingUser) throw new ConflictError('Email already registered');
93
+
94
+ // Hash password
95
+ const hashedPassword = await hashPassword(password);
96
+
97
+ // TODO: Create user in your database
98
+ // const user = await prisma.user.create({
99
+ // data: { email, password: hashedPassword, name }
100
+ // });
101
+
102
+ // Placeholder - replace with actual user creation
103
+ const user: UserData = {
104
+ id: 'user_' + Date.now(), // Replace with actual ID
105
+ email,
106
+ name,
107
+ };
108
+
109
+ return user;
110
+ }
111
+
112
+ /**
113
+ * Example: Login user
114
+ *
115
+ * NOTE: This is a template. Implement your own user lookup logic.
116
+ */
117
+ export async function loginUser(
118
+ input: LoginInput
119
+ ): Promise<{ user: UserData; tokens: AuthTokens }> {
120
+ const { email, password } = input;
121
+
122
+ // TODO: Find user by email in your database
123
+ // const user = await prisma.user.findUnique({ where: { email } });
124
+ // if (!user) throw new UnauthorizedError('Invalid credentials');
125
+
126
+ // TODO: Verify password
127
+ // const isValid = await comparePassword(password, user.password);
128
+ // if (!isValid) throw new UnauthorizedError('Invalid credentials');
129
+
130
+ // Placeholder - replace with actual user lookup
131
+ const user: UserData = {
132
+ id: 'user_1',
133
+ email,
134
+ name: 'User',
135
+ };
136
+
137
+ const tokens = generateAuthTokens(user);
138
+
139
+ return { user, tokens };
140
+ }
@@ -0,0 +1,127 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+
3
+ /**
4
+ * Custom error class for application errors
5
+ */
6
+ export class AppError extends Error {
7
+ public readonly statusCode: number;
8
+ public readonly isOperational: boolean;
9
+ public readonly code?: string;
10
+
11
+ constructor(
12
+ message: string,
13
+ statusCode: number = 500,
14
+ isOperational: boolean = true,
15
+ code?: string
16
+ ) {
17
+ super(message);
18
+ this.statusCode = statusCode;
19
+ this.isOperational = isOperational;
20
+ this.code = code;
21
+
22
+ Object.setPrototypeOf(this, AppError.prototype);
23
+ Error.captureStackTrace(this, this.constructor);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Common HTTP error classes
29
+ */
30
+ export class NotFoundError extends AppError {
31
+ constructor(message: string = 'Resource not found') {
32
+ super(message, 404, true, 'NOT_FOUND');
33
+ }
34
+ }
35
+
36
+ export class BadRequestError extends AppError {
37
+ constructor(message: string = 'Bad request') {
38
+ super(message, 400, true, 'BAD_REQUEST');
39
+ }
40
+ }
41
+
42
+ export class UnauthorizedError extends AppError {
43
+ constructor(message: string = 'Unauthorized') {
44
+ super(message, 401, true, 'UNAUTHORIZED');
45
+ }
46
+ }
47
+
48
+ export class ForbiddenError extends AppError {
49
+ constructor(message: string = 'Forbidden') {
50
+ super(message, 403, true, 'FORBIDDEN');
51
+ }
52
+ }
53
+
54
+ export class ConflictError extends AppError {
55
+ constructor(message: string = 'Conflict') {
56
+ super(message, 409, true, 'CONFLICT');
57
+ }
58
+ }
59
+
60
+ export class ValidationError extends AppError {
61
+ public readonly errors: Record<string, string[]>;
62
+
63
+ constructor(errors: Record<string, string[]>) {
64
+ super('Validation failed', 422, true, 'VALIDATION_ERROR');
65
+ this.errors = errors;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Global error handler middleware
71
+ */
72
+ export function errorHandler(
73
+ err: Error,
74
+ _req: Request,
75
+ res: Response,
76
+ _next: NextFunction
77
+ ): void {
78
+ // Log error for debugging
79
+ console.error('[Error]', err);
80
+
81
+ // Handle AppError instances
82
+ if (err instanceof AppError) {
83
+ const response: Record<string, unknown> = {
84
+ success: false,
85
+ error: {
86
+ message: err.message,
87
+ code: err.code,
88
+ },
89
+ };
90
+
91
+ // Include validation errors if present
92
+ if (err instanceof ValidationError) {
93
+ response.error = {
94
+ ...response.error as object,
95
+ details: err.errors,
96
+ };
97
+ }
98
+
99
+ res.status(err.statusCode).json(response);
100
+ return;
101
+ }
102
+
103
+ // Handle unexpected errors
104
+ const statusCode = 500;
105
+ const message = process.env.NODE_ENV === 'production'
106
+ ? 'Internal server error'
107
+ : err.message;
108
+
109
+ res.status(statusCode).json({
110
+ success: false,
111
+ error: {
112
+ message,
113
+ code: 'INTERNAL_ERROR',
114
+ },
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Async handler wrapper to catch errors in async route handlers
120
+ */
121
+ export function asyncHandler(
122
+ fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
123
+ ) {
124
+ return (req: Request, res: Response, next: NextFunction) => {
125
+ Promise.resolve(fn(req, res, next)).catch(next);
126
+ };
127
+ }
@@ -0,0 +1,104 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import { z, ZodError, ZodSchema } from 'zod';
3
+ import { ValidationError } from './error-handler';
4
+
5
+ type RequestLocation = 'body' | 'query' | 'params';
6
+
7
+ interface ValidateOptions {
8
+ body?: ZodSchema;
9
+ query?: ZodSchema;
10
+ params?: ZodSchema;
11
+ }
12
+
13
+ /**
14
+ * Format Zod errors into a readable format
15
+ */
16
+ function formatZodErrors(error: ZodError): Record<string, string[]> {
17
+ const errors: Record<string, string[]> = {};
18
+
19
+ for (const issue of error.issues) {
20
+ const path = issue.path.join('.');
21
+ const key = path || 'root';
22
+
23
+ if (!errors[key]) {
24
+ errors[key] = [];
25
+ }
26
+
27
+ errors[key].push(issue.message);
28
+ }
29
+
30
+ return errors;
31
+ }
32
+
33
+ /**
34
+ * Validate request data against Zod schemas
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * import { z } from 'zod';
39
+ *
40
+ * const createUserSchema = z.object({
41
+ * email: z.string().email(),
42
+ * password: z.string().min(8),
43
+ * });
44
+ *
45
+ * router.post('/users', validate({ body: createUserSchema }), createUser);
46
+ * ```
47
+ */
48
+ export function validate(schemas: ValidateOptions) {
49
+ return async (req: Request, _res: Response, next: NextFunction) => {
50
+ const allErrors: Record<string, string[]> = {};
51
+
52
+ const locations: RequestLocation[] = ['body', 'query', 'params'];
53
+
54
+ for (const location of locations) {
55
+ const schema = schemas[location];
56
+
57
+ if (schema) {
58
+ const result = await schema.safeParseAsync(req[location]);
59
+
60
+ if (!result.success) {
61
+ const errors = formatZodErrors(result.error);
62
+
63
+ for (const [key, messages] of Object.entries(errors)) {
64
+ const prefixedKey = `${location}.${key}`;
65
+ allErrors[prefixedKey] = messages;
66
+ }
67
+ } else {
68
+ // Replace request data with parsed data (for type coercion)
69
+ req[location] = result.data;
70
+ }
71
+ }
72
+ }
73
+
74
+ if (Object.keys(allErrors).length > 0) {
75
+ return next(new ValidationError(allErrors));
76
+ }
77
+
78
+ next();
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Validate only request body
84
+ */
85
+ export function validateBody<T extends ZodSchema>(schema: T) {
86
+ return validate({ body: schema });
87
+ }
88
+
89
+ /**
90
+ * Validate only query parameters
91
+ */
92
+ export function validateQuery<T extends ZodSchema>(schema: T) {
93
+ return validate({ query: schema });
94
+ }
95
+
96
+ /**
97
+ * Validate only route parameters
98
+ */
99
+ export function validateParams<T extends ZodSchema>(schema: T) {
100
+ return validate({ params: schema });
101
+ }
102
+
103
+ // Re-export Zod for convenience
104
+ export { z };
@@ -0,0 +1,83 @@
1
+ import { prisma, disconnectDb, checkDbConnection } from './prisma';
2
+
3
+ /**
4
+ * Database utilities and helpers
5
+ */
6
+
7
+ /**
8
+ * Transaction wrapper for database operations
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const result = await withTransaction(async (tx) => {
13
+ * const user = await tx.user.create({ data: { email: 'test@example.com' } });
14
+ * const profile = await tx.profile.create({ data: { userId: user.id } });
15
+ * return { user, profile };
16
+ * });
17
+ * ```
18
+ */
19
+ export async function withTransaction<T>(
20
+ fn: (tx: typeof prisma) => Promise<T>
21
+ ): Promise<T> {
22
+ return prisma.$transaction(async (tx) => {
23
+ return fn(tx as typeof prisma);
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Pagination helper
29
+ */
30
+ export interface PaginationOptions {
31
+ page?: number;
32
+ limit?: number;
33
+ }
34
+
35
+ export interface PaginatedResult<T> {
36
+ data: T[];
37
+ meta: {
38
+ total: number;
39
+ page: number;
40
+ limit: number;
41
+ totalPages: number;
42
+ hasNextPage: boolean;
43
+ hasPrevPage: boolean;
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Create pagination parameters for Prisma queries
49
+ */
50
+ export function getPaginationParams(options: PaginationOptions = {}) {
51
+ const page = Math.max(1, options.page || 1);
52
+ const limit = Math.min(100, Math.max(1, options.limit || 10));
53
+ const skip = (page - 1) * limit;
54
+
55
+ return { skip, take: limit, page, limit };
56
+ }
57
+
58
+ /**
59
+ * Format paginated response
60
+ */
61
+ export function formatPaginatedResponse<T>(
62
+ data: T[],
63
+ total: number,
64
+ page: number,
65
+ limit: number
66
+ ): PaginatedResult<T> {
67
+ const totalPages = Math.ceil(total / limit);
68
+
69
+ return {
70
+ data,
71
+ meta: {
72
+ total,
73
+ page,
74
+ limit,
75
+ totalPages,
76
+ hasNextPage: page < totalPages,
77
+ hasPrevPage: page > 1,
78
+ },
79
+ };
80
+ }
81
+
82
+ // Re-export prisma utilities
83
+ export { prisma, disconnectDb, checkDbConnection };
@@ -0,0 +1,47 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ /**
4
+ * Prisma Client Singleton
5
+ *
6
+ * This ensures only one instance of PrismaClient is created,
7
+ * even during hot reloading in development.
8
+ */
9
+
10
+ const globalForPrisma = globalThis as unknown as {
11
+ prisma: PrismaClient | undefined;
12
+ };
13
+
14
+ export const prisma =
15
+ globalForPrisma.prisma ??
16
+ new PrismaClient({
17
+ log:
18
+ process.env.NODE_ENV === 'development'
19
+ ? ['query', 'error', 'warn']
20
+ : ['error'],
21
+ });
22
+
23
+ if (process.env.NODE_ENV !== 'production') {
24
+ globalForPrisma.prisma = prisma;
25
+ }
26
+
27
+ /**
28
+ * Graceful shutdown handler
29
+ * Call this when your app is shutting down
30
+ */
31
+ export async function disconnectDb(): Promise<void> {
32
+ await prisma.$disconnect();
33
+ }
34
+
35
+ /**
36
+ * Health check for database connection
37
+ */
38
+ export async function checkDbConnection(): Promise<boolean> {
39
+ try {
40
+ await prisma.$queryRaw`SELECT 1`;
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ export default prisma;
@@ -0,0 +1,61 @@
1
+ import pinoHttp from 'pino-http';
2
+ import { logger } from './logger';
3
+
4
+ /**
5
+ * HTTP request logging middleware
6
+ *
7
+ * Logs all incoming requests with:
8
+ * - Method, URL, status code
9
+ * - Response time
10
+ * - Request ID for tracing
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * import express from 'express';
15
+ * import { httpLogger } from './lib/setu/logger/http-logger';
16
+ *
17
+ * const app = express();
18
+ * app.use(httpLogger);
19
+ * ```
20
+ */
21
+ export const httpLogger = pinoHttp({
22
+ logger,
23
+
24
+ // Generate unique request ID
25
+ genReqId: (req) => {
26
+ return req.headers['x-request-id'] as string || crypto.randomUUID();
27
+ },
28
+
29
+ // Custom log level based on status code
30
+ customLogLevel: (_req, res, err) => {
31
+ if (res.statusCode >= 500 || err) return 'error';
32
+ if (res.statusCode >= 400) return 'warn';
33
+ return 'info';
34
+ },
35
+
36
+ // Custom success message
37
+ customSuccessMessage: (req, res) => {
38
+ return `${req.method} ${req.url} ${res.statusCode}`;
39
+ },
40
+
41
+ // Custom error message
42
+ customErrorMessage: (req, res, err) => {
43
+ return `${req.method} ${req.url} ${res.statusCode} - ${err.message}`;
44
+ },
45
+
46
+ // Properties to include in logs
47
+ customProps: (req) => ({
48
+ userAgent: req.headers['user-agent'],
49
+ ip: req.headers['x-forwarded-for'] || req.socket.remoteAddress,
50
+ }),
51
+
52
+ // Don't log these paths (health checks, etc.)
53
+ autoLogging: {
54
+ ignore: (req) => {
55
+ const ignorePaths = ['/health', '/ready', '/metrics'];
56
+ return ignorePaths.includes(req.url || '');
57
+ },
58
+ },
59
+ });
60
+
61
+ export default httpLogger;
@@ -0,0 +1,60 @@
1
+ import pino from 'pino';
2
+
3
+ /**
4
+ * Logger configuration
5
+ *
6
+ * In production, logs are JSON formatted for easy parsing.
7
+ * In development, logs are pretty-printed for readability.
8
+ */
9
+ const isProduction = process.env.NODE_ENV === 'production';
10
+
11
+ export const logger = pino({
12
+ level: process.env.LOG_LEVEL || (isProduction ? 'info' : 'debug'),
13
+
14
+ // Base properties added to every log
15
+ base: {
16
+ pid: process.pid,
17
+ env: process.env.NODE_ENV || 'development',
18
+ },
19
+
20
+ // Timestamp configuration
21
+ timestamp: pino.stdTimeFunctions.isoTime,
22
+
23
+ // Pretty print in development
24
+ transport: isProduction
25
+ ? undefined
26
+ : {
27
+ target: 'pino-pretty',
28
+ options: {
29
+ colorize: true,
30
+ translateTime: 'SYS:standard',
31
+ ignore: 'pid,hostname',
32
+ },
33
+ },
34
+ });
35
+
36
+ /**
37
+ * Create a child logger with additional context
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const userLogger = createLogger({ module: 'user-service' });
42
+ * userLogger.info({ userId: '123' }, 'User logged in');
43
+ * ```
44
+ */
45
+ export function createLogger(context: Record<string, unknown>) {
46
+ return logger.child(context);
47
+ }
48
+
49
+ /**
50
+ * Log levels available:
51
+ * - trace: Very detailed debugging information
52
+ * - debug: Debugging information
53
+ * - info: General information
54
+ * - warn: Warning messages
55
+ * - error: Error messages
56
+ * - fatal: Critical errors
57
+ */
58
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
59
+
60
+ export default logger;
@@ -0,0 +1,88 @@
1
+ import helmet from 'helmet';
2
+
3
+ /**
4
+ * Helmet security middleware configuration
5
+ *
6
+ * Sets various HTTP headers to protect your app from common vulnerabilities.
7
+ *
8
+ * @see https://helmetjs.github.io/
9
+ */
10
+ export const helmetConfig = helmet({
11
+ // Content Security Policy
12
+ contentSecurityPolicy: {
13
+ directives: {
14
+ defaultSrc: ["'self'"],
15
+ styleSrc: ["'self'", "'unsafe-inline'"],
16
+ scriptSrc: ["'self'"],
17
+ imgSrc: ["'self'", 'data:', 'https:'],
18
+ connectSrc: ["'self'"],
19
+ fontSrc: ["'self'"],
20
+ objectSrc: ["'none'"],
21
+ mediaSrc: ["'self'"],
22
+ frameSrc: ["'none'"],
23
+ },
24
+ },
25
+
26
+ // Cross-Origin Resource Policy
27
+ crossOriginResourcePolicy: { policy: 'same-origin' },
28
+
29
+ // Cross-Origin Opener Policy
30
+ crossOriginOpenerPolicy: { policy: 'same-origin' },
31
+
32
+ // Cross-Origin Embedder Policy
33
+ crossOriginEmbedderPolicy: false, // Enable if using SharedArrayBuffer
34
+
35
+ // DNS Prefetch Control
36
+ dnsPrefetchControl: { allow: false },
37
+
38
+ // Frameguard (X-Frame-Options)
39
+ frameguard: { action: 'deny' },
40
+
41
+ // Hide X-Powered-By header
42
+ hidePoweredBy: true,
43
+
44
+ // HTTP Strict Transport Security
45
+ hsts: {
46
+ maxAge: 31536000, // 1 year
47
+ includeSubDomains: true,
48
+ preload: true,
49
+ },
50
+
51
+ // IE No Open
52
+ ieNoOpen: true,
53
+
54
+ // Don't Sniff Mimetype
55
+ noSniff: true,
56
+
57
+ // Origin Agent Cluster
58
+ originAgentCluster: true,
59
+
60
+ // Permitted Cross-Domain Policies
61
+ permittedCrossDomainPolicies: { permittedPolicies: 'none' },
62
+
63
+ // Referrer Policy
64
+ referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
65
+
66
+ // X-XSS-Protection (deprecated but still useful for older browsers)
67
+ xssFilter: true,
68
+ });
69
+
70
+ /**
71
+ * Relaxed helmet config for development
72
+ * Disables CSP which can be annoying during development
73
+ */
74
+ export const helmetDevConfig = helmet({
75
+ contentSecurityPolicy: false,
76
+ crossOriginEmbedderPolicy: false,
77
+ });
78
+
79
+ /**
80
+ * Get appropriate helmet config based on environment
81
+ */
82
+ export function getHelmetConfig() {
83
+ return process.env.NODE_ENV === 'production'
84
+ ? helmetConfig
85
+ : helmetDevConfig;
86
+ }
87
+
88
+ export default helmetConfig;