create-listablelabs-api 1.0.1

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.
Files changed (32) hide show
  1. package/README.md +142 -0
  2. package/bin/cli.js +37 -0
  3. package/bin/commands/add.js +460 -0
  4. package/bin/commands/create.js +481 -0
  5. package/package.json +39 -0
  6. package/templates/base/.dockerignore +19 -0
  7. package/templates/base/.env.example +18 -0
  8. package/templates/base/.eslintrc.js +31 -0
  9. package/templates/base/Dockerfile +48 -0
  10. package/templates/base/README.md +295 -0
  11. package/templates/base/docker-compose.yml +55 -0
  12. package/templates/base/jest.config.js +24 -0
  13. package/templates/base/package.json +41 -0
  14. package/templates/base/src/app.js +103 -0
  15. package/templates/base/src/config/index.js +36 -0
  16. package/templates/base/src/controllers/exampleController.js +148 -0
  17. package/templates/base/src/database/baseModel.js +160 -0
  18. package/templates/base/src/database/index.js +108 -0
  19. package/templates/base/src/middlewares/errorHandler.js +155 -0
  20. package/templates/base/src/middlewares/index.js +49 -0
  21. package/templates/base/src/middlewares/rateLimiter.js +85 -0
  22. package/templates/base/src/middlewares/requestLogger.js +50 -0
  23. package/templates/base/src/middlewares/validator.js +107 -0
  24. package/templates/base/src/models/example.js +117 -0
  25. package/templates/base/src/models/index.js +6 -0
  26. package/templates/base/src/routes/v1/exampleRoutes.js +89 -0
  27. package/templates/base/src/routes/v1/index.js +19 -0
  28. package/templates/base/src/server.js +80 -0
  29. package/templates/base/src/utils/logger.js +61 -0
  30. package/templates/base/src/utils/response.js +117 -0
  31. package/templates/base/tests/app.test.js +215 -0
  32. package/templates/base/tests/setup.js +33 -0
@@ -0,0 +1,160 @@
1
+ const mongoose = require('mongoose');
2
+
3
+ /**
4
+ * Base schema options applied to all models
5
+ */
6
+ const baseSchemaOptions = {
7
+ timestamps: true,
8
+ toJSON: {
9
+ virtuals: true,
10
+ transform: (doc, ret) => {
11
+ ret.id = ret._id.toString();
12
+ delete ret._id;
13
+ delete ret.__v;
14
+ return ret;
15
+ },
16
+ },
17
+ toObject: {
18
+ virtuals: true,
19
+ },
20
+ };
21
+
22
+ /**
23
+ * Common plugins for all schemas
24
+ */
25
+ const paginatePlugin = (schema) => {
26
+ /**
27
+ * Paginate query results
28
+ * @param {Object} filter - Mongoose filter query
29
+ * @param {Object} options - Pagination options
30
+ * @param {number} options.page - Current page (default: 1)
31
+ * @param {number} options.limit - Results per page (default: 20)
32
+ * @param {string} options.sortBy - Sort field (default: createdAt)
33
+ * @param {string} options.sortOrder - Sort order 'asc' or 'desc' (default: desc)
34
+ * @param {string} options.populate - Fields to populate
35
+ */
36
+ schema.statics.paginate = async function (filter = {}, options = {}) {
37
+ const {
38
+ page = 1,
39
+ limit = 20,
40
+ sortBy = 'createdAt',
41
+ sortOrder = 'desc',
42
+ populate = '',
43
+ } = options;
44
+
45
+ const skip = (page - 1) * limit;
46
+ const sort = { [sortBy]: sortOrder === 'asc' ? 1 : -1 };
47
+
48
+ const [data, total] = await Promise.all([
49
+ this.find(filter)
50
+ .sort(sort)
51
+ .skip(skip)
52
+ .limit(limit)
53
+ .populate(populate)
54
+ .lean(),
55
+ this.countDocuments(filter),
56
+ ]);
57
+
58
+ return {
59
+ data,
60
+ pagination: {
61
+ page,
62
+ limit,
63
+ total,
64
+ totalPages: Math.ceil(total / limit),
65
+ hasNextPage: page < Math.ceil(total / limit),
66
+ hasPrevPage: page > 1,
67
+ },
68
+ };
69
+ };
70
+ };
71
+
72
+ /**
73
+ * Soft delete plugin
74
+ */
75
+ const softDeletePlugin = (schema) => {
76
+ schema.add({
77
+ isDeleted: { type: Boolean, default: false },
78
+ deletedAt: { type: Date, default: null },
79
+ });
80
+
81
+ // Override find methods to exclude soft-deleted docs by default
82
+ schema.pre(/^find/, function (next) {
83
+ if (this.getQuery().includeDeleted !== true) {
84
+ this.where({ isDeleted: false });
85
+ }
86
+ next();
87
+ });
88
+
89
+ schema.methods.softDelete = async function () {
90
+ this.isDeleted = true;
91
+ this.deletedAt = new Date();
92
+ return this.save();
93
+ };
94
+
95
+ schema.methods.restore = async function () {
96
+ this.isDeleted = false;
97
+ this.deletedAt = null;
98
+ return this.save();
99
+ };
100
+
101
+ schema.statics.findDeleted = function () {
102
+ return this.find({ isDeleted: true, includeDeleted: true });
103
+ };
104
+ };
105
+
106
+ /**
107
+ * Create a model with common plugins
108
+ * @param {string} name - Model name
109
+ * @param {Object} schemaDefinition - Schema fields
110
+ * @param {Object} options - Additional options
111
+ */
112
+ const createModel = (name, schemaDefinition, options = {}) => {
113
+ const schema = new mongoose.Schema(schemaDefinition, {
114
+ ...baseSchemaOptions,
115
+ ...options.schemaOptions,
116
+ });
117
+
118
+ // Apply plugins
119
+ schema.plugin(paginatePlugin);
120
+
121
+ if (options.softDelete) {
122
+ schema.plugin(softDeletePlugin);
123
+ }
124
+
125
+ // Add indexes if provided
126
+ if (options.indexes) {
127
+ options.indexes.forEach((index) => {
128
+ schema.index(index.fields, index.options);
129
+ });
130
+ }
131
+
132
+ // Add methods if provided
133
+ if (options.methods) {
134
+ Object.assign(schema.methods, options.methods);
135
+ }
136
+
137
+ // Add statics if provided
138
+ if (options.statics) {
139
+ Object.assign(schema.statics, options.statics);
140
+ }
141
+
142
+ // Add virtuals if provided
143
+ if (options.virtuals) {
144
+ Object.entries(options.virtuals).forEach(([name, config]) => {
145
+ const virtual = schema.virtual(name);
146
+ if (config.get) virtual.get(config.get);
147
+ if (config.set) virtual.set(config.set);
148
+ });
149
+ }
150
+
151
+ return mongoose.model(name, schema);
152
+ };
153
+
154
+ module.exports = {
155
+ baseSchemaOptions,
156
+ paginatePlugin,
157
+ softDeletePlugin,
158
+ createModel,
159
+ mongoose,
160
+ };
@@ -0,0 +1,108 @@
1
+ const mongoose = require('mongoose');
2
+ const { logger } = require('../utils/logger');
3
+ const config = require('../config');
4
+
5
+ /**
6
+ * MongoDB Atlas Connection
7
+ *
8
+ * Connection string format:
9
+ * mongodb+srv://<username>:<password>@<cluster>.mongodb.net/<database>?retryWrites=true&w=majority
10
+ */
11
+
12
+ const connectDB = async () => {
13
+ try {
14
+ const conn = await mongoose.connect(config.mongoUri, {
15
+ // Connection pool settings
16
+ maxPoolSize: 10,
17
+ minPoolSize: 2,
18
+
19
+ // Timeouts
20
+ serverSelectionTimeoutMS: 5000,
21
+ socketTimeoutMS: 45000,
22
+
23
+ // Buffering
24
+ bufferCommands: false,
25
+ });
26
+
27
+ logger.info({
28
+ host: conn.connection.host,
29
+ name: conn.connection.name,
30
+ }, '✅ MongoDB Atlas connected');
31
+
32
+ return conn;
33
+ } catch (err) {
34
+ logger.fatal({ err }, '❌ MongoDB connection failed');
35
+ process.exit(1);
36
+ }
37
+ };
38
+
39
+ /**
40
+ * Connection event handlers
41
+ */
42
+ mongoose.connection.on('connected', () => {
43
+ logger.debug('Mongoose connected to MongoDB');
44
+ });
45
+
46
+ mongoose.connection.on('disconnected', () => {
47
+ logger.warn('Mongoose disconnected from MongoDB');
48
+ });
49
+
50
+ mongoose.connection.on('error', (err) => {
51
+ logger.error({ err }, 'Mongoose connection error');
52
+ });
53
+
54
+ /**
55
+ * Graceful shutdown
56
+ */
57
+ const disconnectDB = async () => {
58
+ try {
59
+ await mongoose.connection.close();
60
+ logger.info('MongoDB connection closed');
61
+ } catch (err) {
62
+ logger.error({ err }, 'Error closing MongoDB connection');
63
+ }
64
+ };
65
+
66
+ /**
67
+ * Health check
68
+ */
69
+ const healthCheck = async () => {
70
+ return mongoose.connection.readyState === 1;
71
+ };
72
+
73
+ /**
74
+ * Get connection status for health endpoints
75
+ */
76
+ const getConnectionStatus = () => {
77
+ const state = mongoose.connection.readyState;
78
+ const states = {
79
+ 0: 'disconnected',
80
+ 1: 'connected',
81
+ 2: 'connecting',
82
+ 3: 'disconnecting',
83
+ };
84
+
85
+ return states[state] || 'unknown';
86
+ };
87
+
88
+ /**
89
+ * Get connection stats
90
+ */
91
+ const getStats = () => {
92
+ const { connection } = mongoose;
93
+ return {
94
+ readyState: connection.readyState,
95
+ host: connection.host,
96
+ name: connection.name,
97
+ collections: Object.keys(connection.collections),
98
+ };
99
+ };
100
+
101
+ module.exports = {
102
+ connectDB,
103
+ disconnectDB,
104
+ healthCheck,
105
+ getConnectionStatus,
106
+ getStats,
107
+ mongoose,
108
+ };
@@ -0,0 +1,155 @@
1
+ const { StatusCodes, ReasonPhrases } = require('http-status-codes');
2
+ const { logger } = require('../utils/logger');
3
+
4
+ /**
5
+ * Custom Application Error class
6
+ * Use this for all operational errors (expected errors)
7
+ */
8
+ class AppError extends Error {
9
+ constructor(message, statusCode, errorCode = null) {
10
+ super(message);
11
+ this.statusCode = statusCode;
12
+ this.errorCode = errorCode || this.constructor.name;
13
+ this.isOperational = true; // Distinguishes operational errors from programming errors
14
+ this.timestamp = new Date().toISOString();
15
+
16
+ Error.captureStackTrace(this, this.constructor);
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Common error types for consistent error handling
22
+ */
23
+ class BadRequestError extends AppError {
24
+ constructor(message = 'Bad Request') {
25
+ super(message, StatusCodes.BAD_REQUEST, 'BAD_REQUEST');
26
+ }
27
+ }
28
+
29
+ class UnauthorizedError extends AppError {
30
+ constructor(message = 'Unauthorized') {
31
+ super(message, StatusCodes.UNAUTHORIZED, 'UNAUTHORIZED');
32
+ }
33
+ }
34
+
35
+ class ForbiddenError extends AppError {
36
+ constructor(message = 'Forbidden') {
37
+ super(message, StatusCodes.FORBIDDEN, 'FORBIDDEN');
38
+ }
39
+ }
40
+
41
+ class NotFoundError extends AppError {
42
+ constructor(message = 'Resource not found') {
43
+ super(message, StatusCodes.NOT_FOUND, 'NOT_FOUND');
44
+ }
45
+ }
46
+
47
+ class ConflictError extends AppError {
48
+ constructor(message = 'Resource conflict') {
49
+ super(message, StatusCodes.CONFLICT, 'CONFLICT');
50
+ }
51
+ }
52
+
53
+ class ValidationError extends AppError {
54
+ constructor(message = 'Validation failed', details = []) {
55
+ super(message, StatusCodes.UNPROCESSABLE_ENTITY, 'VALIDATION_ERROR');
56
+ this.details = details;
57
+ }
58
+ }
59
+
60
+ class TooManyRequestsError extends AppError {
61
+ constructor(message = 'Too many requests') {
62
+ super(message, StatusCodes.TOO_MANY_REQUESTS, 'RATE_LIMIT_EXCEEDED');
63
+ }
64
+ }
65
+
66
+ class InternalServerError extends AppError {
67
+ constructor(message = 'Internal server error') {
68
+ super(message, StatusCodes.INTERNAL_SERVER_ERROR, 'INTERNAL_ERROR');
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Centralized error handler middleware
74
+ * Must be registered LAST in middleware chain
75
+ */
76
+ const errorHandler = (err, req, res, next) => {
77
+ // Default values for unknown errors
78
+ let statusCode = err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR;
79
+ let message = err.message || ReasonPhrases.INTERNAL_SERVER_ERROR;
80
+ let errorCode = err.errorCode || 'INTERNAL_ERROR';
81
+
82
+ // Log the error
83
+ const errorLog = {
84
+ requestId: req.id,
85
+ method: req.method,
86
+ path: req.originalUrl,
87
+ statusCode,
88
+ errorCode,
89
+ message: err.message,
90
+ stack: err.stack,
91
+ isOperational: err.isOperational || false,
92
+ };
93
+
94
+ // Log level based on error type
95
+ if (err.isOperational) {
96
+ logger.warn(errorLog, 'Operational error occurred');
97
+ } else {
98
+ logger.error(errorLog, 'Unexpected error occurred');
99
+ }
100
+
101
+ // Don't leak internal details in production
102
+ if (process.env.NODE_ENV === 'production' && !err.isOperational) {
103
+ message = ReasonPhrases.INTERNAL_SERVER_ERROR;
104
+ errorCode = 'INTERNAL_ERROR';
105
+ }
106
+
107
+ // Build error response
108
+ const errorResponse = {
109
+ success: false,
110
+ error: {
111
+ code: errorCode,
112
+ message,
113
+ ...(err.details && { details: err.details }),
114
+ },
115
+ requestId: req.id,
116
+ timestamp: new Date().toISOString(),
117
+ };
118
+
119
+ // Include stack trace in development
120
+ if (process.env.NODE_ENV === 'development') {
121
+ errorResponse.error.stack = err.stack;
122
+ }
123
+
124
+ res.status(statusCode).json(errorResponse);
125
+ };
126
+
127
+ /**
128
+ * Async handler wrapper to catch errors in async route handlers
129
+ * Usage: router.get('/path', asyncHandler(async (req, res) => { ... }))
130
+ */
131
+ const asyncHandler = (fn) => (req, res, next) => {
132
+ Promise.resolve(fn(req, res, next)).catch(next);
133
+ };
134
+
135
+ /**
136
+ * 404 Not Found handler for undefined routes
137
+ */
138
+ const notFoundHandler = (req, res, next) => {
139
+ next(new NotFoundError(`Route ${req.method} ${req.originalUrl} not found`));
140
+ };
141
+
142
+ module.exports = {
143
+ AppError,
144
+ BadRequestError,
145
+ UnauthorizedError,
146
+ ForbiddenError,
147
+ NotFoundError,
148
+ ConflictError,
149
+ ValidationError,
150
+ TooManyRequestsError,
151
+ InternalServerError,
152
+ errorHandler,
153
+ asyncHandler,
154
+ notFoundHandler,
155
+ };
@@ -0,0 +1,49 @@
1
+ const requestLogger = require('./requestLogger');
2
+ const {
3
+ AppError,
4
+ BadRequestError,
5
+ UnauthorizedError,
6
+ ForbiddenError,
7
+ NotFoundError,
8
+ ConflictError,
9
+ ValidationError,
10
+ TooManyRequestsError,
11
+ InternalServerError,
12
+ errorHandler,
13
+ asyncHandler,
14
+ notFoundHandler,
15
+ } = require('./errorHandler');
16
+ const { validate, schemas, z, makePartial, pick, omit } = require('./validator');
17
+ const { defaultLimiter, strictLimiter, createLimiter } = require('./rateLimiter');
18
+
19
+ module.exports = {
20
+ // Logging
21
+ requestLogger,
22
+
23
+ // Error handling
24
+ AppError,
25
+ BadRequestError,
26
+ UnauthorizedError,
27
+ ForbiddenError,
28
+ NotFoundError,
29
+ ConflictError,
30
+ ValidationError,
31
+ TooManyRequestsError,
32
+ InternalServerError,
33
+ errorHandler,
34
+ asyncHandler,
35
+ notFoundHandler,
36
+
37
+ // Validation (Zod)
38
+ validate,
39
+ schemas,
40
+ z,
41
+ makePartial,
42
+ pick,
43
+ omit,
44
+
45
+ // Rate limiting
46
+ defaultLimiter,
47
+ strictLimiter,
48
+ createLimiter,
49
+ };
@@ -0,0 +1,85 @@
1
+ const rateLimit = require('express-rate-limit');
2
+ const config = require('../config');
3
+ const { TooManyRequestsError } = require('./errorHandler');
4
+ const { logger } = require('../utils/logger');
5
+
6
+ /**
7
+ * Default rate limiter
8
+ * Applies to all routes unless overridden
9
+ */
10
+ const defaultLimiter = rateLimit({
11
+ windowMs: config.rateLimit.windowMs,
12
+ max: config.rateLimit.maxRequests,
13
+ standardHeaders: true, // Return rate limit info in headers
14
+ legacyHeaders: false, // Disable X-RateLimit headers
15
+
16
+ // Custom key generator - can be modified for user-based limiting
17
+ keyGenerator: (req) => {
18
+ return req.ip || req.headers['x-forwarded-for'] || 'unknown';
19
+ },
20
+
21
+ // Skip rate limiting for health checks
22
+ skip: (req) => {
23
+ return req.path === '/health' || req.path === '/ready';
24
+ },
25
+
26
+ // Custom handler when limit is exceeded
27
+ handler: (req, res, next) => {
28
+ logger.warn({
29
+ requestId: req.id,
30
+ ip: req.ip,
31
+ path: req.originalUrl,
32
+ }, 'Rate limit exceeded');
33
+
34
+ next(new TooManyRequestsError('Too many requests, please try again later'));
35
+ },
36
+ });
37
+
38
+ /**
39
+ * Strict rate limiter for sensitive endpoints (auth, etc.)
40
+ */
41
+ const strictLimiter = rateLimit({
42
+ windowMs: 15 * 60 * 1000, // 15 minutes
43
+ max: 5, // 5 requests per window
44
+ standardHeaders: true,
45
+ legacyHeaders: false,
46
+
47
+ handler: (req, res, next) => {
48
+ logger.warn({
49
+ requestId: req.id,
50
+ ip: req.ip,
51
+ path: req.originalUrl,
52
+ }, 'Strict rate limit exceeded');
53
+
54
+ next(new TooManyRequestsError('Too many attempts, please try again later'));
55
+ },
56
+ });
57
+
58
+ /**
59
+ * Create custom rate limiter with specific options
60
+ *
61
+ * @param {Object} options - Rate limiter options
62
+ * @returns {Function} Rate limiter middleware
63
+ */
64
+ const createLimiter = (options) => {
65
+ return rateLimit({
66
+ standardHeaders: true,
67
+ legacyHeaders: false,
68
+ handler: (req, res, next) => {
69
+ logger.warn({
70
+ requestId: req.id,
71
+ ip: req.ip,
72
+ path: req.originalUrl,
73
+ }, 'Custom rate limit exceeded');
74
+
75
+ next(new TooManyRequestsError(options.message || 'Rate limit exceeded'));
76
+ },
77
+ ...options,
78
+ });
79
+ };
80
+
81
+ module.exports = {
82
+ defaultLimiter,
83
+ strictLimiter,
84
+ createLimiter,
85
+ };
@@ -0,0 +1,50 @@
1
+ const pinoHttp = require('pino-http');
2
+ const { v4: uuidv4 } = require('uuid');
3
+ const { logger } = require('../utils/logger');
4
+
5
+ const requestLogger = pinoHttp({
6
+ logger,
7
+
8
+ // Generate unique request ID
9
+ genReqId: (req) => {
10
+ return req.headers['x-request-id'] || uuidv4();
11
+ },
12
+
13
+ // Custom log level based on response status
14
+ customLogLevel: (req, res, err) => {
15
+ if (res.statusCode >= 500 || err) return 'error';
16
+ if (res.statusCode >= 400) return 'warn';
17
+ return 'info';
18
+ },
19
+
20
+ // Custom success message
21
+ customSuccessMessage: (req, res) => {
22
+ return `${req.method} ${req.url} completed`;
23
+ },
24
+
25
+ // Custom error message
26
+ customErrorMessage: (req, res, err) => {
27
+ return `${req.method} ${req.url} failed: ${err.message}`;
28
+ },
29
+
30
+ // Custom attributes to add to log
31
+ customAttributeKeys: {
32
+ req: 'request',
33
+ res: 'response',
34
+ err: 'error',
35
+ responseTime: 'duration',
36
+ },
37
+
38
+ // Custom props to add to each log
39
+ customProps: (req, res) => ({
40
+ requestId: req.id,
41
+ userAgent: req.headers['user-agent'],
42
+ }),
43
+
44
+ // Don't log health checks to reduce noise
45
+ autoLogging: {
46
+ ignore: (req) => req.url === '/health' || req.url === '/ready',
47
+ },
48
+ });
49
+
50
+ module.exports = requestLogger;
@@ -0,0 +1,107 @@
1
+ const { z } = require('zod');
2
+ const { ValidationError } = require('./errorHandler');
3
+
4
+ /**
5
+ * Validation middleware factory using Zod
6
+ * Creates middleware that validates request data against Zod schemas
7
+ *
8
+ * @param {Object} schema - Object containing Zod schemas for body, query, params
9
+ * @returns {Function} Express middleware
10
+ *
11
+ * Usage:
12
+ * const schema = {
13
+ * body: z.object({ name: z.string().min(1) }),
14
+ * params: z.object({ id: z.string().uuid() }),
15
+ * query: z.object({ page: z.coerce.number().int().min(1).optional() }),
16
+ * };
17
+ * router.post('/users/:id', validate(schema), controller.updateUser);
18
+ */
19
+ const validate = (schema) => {
20
+ return (req, res, next) => {
21
+ const validationErrors = [];
22
+
23
+ // Validate each part of the request
24
+ ['body', 'query', 'params'].forEach((key) => {
25
+ if (schema[key]) {
26
+ const result = schema[key].safeParse(req[key]);
27
+
28
+ if (!result.success) {
29
+ result.error.errors.forEach((err) => {
30
+ validationErrors.push({
31
+ field: err.path.join('.'),
32
+ message: err.message,
33
+ location: key,
34
+ });
35
+ });
36
+ } else {
37
+ // Replace with validated/transformed values
38
+ req[key] = result.data;
39
+ }
40
+ }
41
+ });
42
+
43
+ if (validationErrors.length > 0) {
44
+ return next(new ValidationError('Validation failed', validationErrors));
45
+ }
46
+
47
+ next();
48
+ };
49
+ };
50
+
51
+ /**
52
+ * Common Zod schema patterns for reuse
53
+ */
54
+ const schemas = {
55
+ // Common field patterns
56
+ id: z.string().min(1),
57
+ mongoId: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid MongoDB ObjectId'),
58
+ uuid: z.string().uuid(),
59
+ email: z.string().email().toLowerCase().trim(),
60
+ password: z.string().min(8).max(128),
61
+ phone: z.string().regex(/^\+?[1-9]\d{1,14}$/, 'Invalid phone number'),
62
+
63
+ // Pagination
64
+ pagination: z.object({
65
+ page: z.coerce.number().int().min(1).default(1),
66
+ limit: z.coerce.number().int().min(1).max(100).default(20),
67
+ sortBy: z.string().optional(),
68
+ sortOrder: z.enum(['asc', 'desc']).default('desc'),
69
+ }),
70
+
71
+ // Date range
72
+ dateRange: z.object({
73
+ startDate: z.coerce.date(),
74
+ endDate: z.coerce.date(),
75
+ }).refine((data) => data.endDate > data.startDate, {
76
+ message: 'End date must be after start date',
77
+ path: ['endDate'],
78
+ }),
79
+
80
+ // Common string transformations
81
+ trimmedString: z.string().trim(),
82
+ nonEmptyString: z.string().min(1).trim(),
83
+ };
84
+
85
+ /**
86
+ * Helper to make all fields in a schema optional (for PATCH requests)
87
+ */
88
+ const makePartial = (schema) => schema.partial();
89
+
90
+ /**
91
+ * Helper to pick specific fields from a schema
92
+ */
93
+ const pick = (schema, keys) => schema.pick(keys.reduce((acc, key) => ({ ...acc, [key]: true }), {}));
94
+
95
+ /**
96
+ * Helper to omit specific fields from a schema
97
+ */
98
+ const omit = (schema, keys) => schema.omit(keys.reduce((acc, key) => ({ ...acc, [key]: true }), {}));
99
+
100
+ module.exports = {
101
+ validate,
102
+ schemas,
103
+ makePartial,
104
+ pick,
105
+ omit,
106
+ z, // Re-export Zod for convenience
107
+ };