zerra-core 1.2.1 → 1.2.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.
Files changed (3) hide show
  1. package/index.d.ts +7 -0
  2. package/index.js +341 -86
  3. package/package.json +6 -2
package/index.d.ts CHANGED
@@ -12,12 +12,15 @@ export interface ZerraRequest extends IncomingMessage {
12
12
  buffer: Buffer;
13
13
  }>;
14
14
  params: Record<string, string>;
15
+ cookies: Record<string, string>; // Feature 1: Parsed Cookies
15
16
  }
16
17
 
17
18
  export interface ZerraResponse extends ServerResponse {
18
19
  status(code: number): ZerraResponse;
19
20
  json(data: any): void;
20
21
  cors(options?: { origin?: string; methods?: string }): ZerraResponse;
22
+ sendFile(filePath: string): void; // Feature 2: Send File helper
23
+ redirect(url: string, status?: number): void; // Feature 3: Redirect helper
21
24
  }
22
25
 
23
26
  export type ZerraHandler = (req: ZerraRequest, res: ZerraResponse) => void | Promise<void>;
@@ -28,12 +31,16 @@ export interface ZerraConfig {
28
31
  features: {
29
32
  logging?: boolean;
30
33
  dynamicRouting?: boolean;
34
+
31
35
  middleware?: boolean;
32
36
  dotenv?: boolean;
33
37
  validation?: boolean;
34
38
  multipart?: boolean;
35
39
  errors?: boolean;
36
40
  dashboard?: boolean;
41
+ static?: boolean; // Feature 4: Static File Serving
42
+ rateLimiting?: boolean | { max: number; windowMs: number }; // Feature 5: Built-in Rate Limiting
43
+ cron?: boolean; // Feature 6: Built-in Cron Job Scheduler
37
44
  };
38
45
  plugins?: string[];
39
46
  }
package/index.js CHANGED
@@ -3,6 +3,7 @@ const fs = require("fs");
3
3
  const path = require("path");
4
4
 
5
5
  function startServer(port = 3000) {
6
+ const isDev = process.env.NODE_ENV !== 'production';
6
7
  const jiti = require("jiti")(__filename);
7
8
  const configPath = path.join(process.cwd(), 'zerra.config.json');
8
9
  let config = {
@@ -14,7 +15,10 @@ function startServer(port = 3000) {
14
15
  validation: true,
15
16
  multipart: true,
16
17
  errors: true,
17
- dashboard: true
18
+ dashboard: true,
19
+ static: true, // Feature 4: Static File Serving
20
+ rateLimiting: false, // Feature 5: Built-in Rate Limiting
21
+ cron: true, // Feature 6: Built-in Cron Job Scheduler
18
22
  },
19
23
  plugins: []
20
24
  };
@@ -27,6 +31,23 @@ function startServer(port = 3000) {
27
31
  }
28
32
  }
29
33
 
34
+ // 1. Optimized module caching mechanism
35
+ const moduleCache = new Map();
36
+ const getModule = (modulePath) => {
37
+ if (isDev) {
38
+ try {
39
+ delete require.cache[require.resolve(modulePath)];
40
+ } catch (e) {}
41
+ return jiti(modulePath);
42
+ }
43
+ if (moduleCache.has(modulePath)) {
44
+ return moduleCache.get(modulePath);
45
+ }
46
+ const mod = jiti(modulePath);
47
+ moduleCache.set(modulePath, mod);
48
+ return mod;
49
+ };
50
+
30
51
  const customEnvKeys = new Set();
31
52
  const recentRequests = [];
32
53
  const MAX_LOGS = 20;
@@ -71,7 +92,7 @@ function startServer(port = 3000) {
71
92
  if (config.plugins && Array.isArray(config.plugins)) {
72
93
  config.plugins.forEach(pluginPath => {
73
94
  try {
74
- const plugin = jiti(path.isAbsolute(pluginPath) ? pluginPath : path.join(process.cwd(), pluginPath));
95
+ const plugin = getModule(path.isAbsolute(pluginPath) ? pluginPath : path.join(process.cwd(), pluginPath));
75
96
  if (typeof plugin === 'function') plugin(zerra);
76
97
  } catch (e) {
77
98
  console.error(`āŒ Failed to load plugin: ${pluginPath}`, e);
@@ -81,6 +102,86 @@ function startServer(port = 3000) {
81
102
 
82
103
  const apiDir = path.join(process.cwd(), "api");
83
104
 
105
+ // In-memory Route Table for fast production route resolution
106
+ let routeTable = [];
107
+
108
+ function buildRouteTable(dir, base = '') {
109
+ let results = [];
110
+ if (!fs.existsSync(dir)) return results;
111
+ const list = fs.readdirSync(dir);
112
+ list.forEach(file => {
113
+ const filePath = path.join(dir, file);
114
+ const stat = fs.statSync(filePath);
115
+ if (stat && stat.isDirectory()) {
116
+ results = results.concat(buildRouteTable(filePath, path.join(base, file)));
117
+ } else if ((file.endsWith('.js') || file.endsWith('.ts')) && !file.startsWith('_')) {
118
+ const relRoute = path.join(base, file).replace(/\\/g, '/').replace(/\.(js|ts)$/, '');
119
+ const segments = relRoute.split('/').filter(Boolean);
120
+
121
+ // Resolve middleware paths for this route
122
+ const middlewarePaths = [];
123
+ if (config.features.middleware) {
124
+ let currentPath = path.dirname(filePath);
125
+ while (currentPath.length >= apiDir.length && currentPath.startsWith(apiDir)) {
126
+ let mwPath = path.join(currentPath, '_middleware.js');
127
+ if (!fs.existsSync(mwPath)) mwPath = path.join(currentPath, '_middleware.ts');
128
+ if (fs.existsSync(mwPath)) {
129
+ middlewarePaths.unshift(mwPath);
130
+ }
131
+ if (currentPath === apiDir) break;
132
+ currentPath = path.dirname(currentPath);
133
+ }
134
+ }
135
+
136
+ results.push({
137
+ filePath,
138
+ segments,
139
+ middlewarePaths
140
+ });
141
+ }
142
+ });
143
+ return results;
144
+ }
145
+
146
+ if (!isDev) {
147
+ routeTable = buildRouteTable(apiDir);
148
+ // Sort exact segments before dynamic parameter segments, and longer paths first
149
+ routeTable.sort((a, b) => {
150
+ const minLen = Math.min(a.segments.length, b.segments.length);
151
+ for (let i = 0; i < minLen; i++) {
152
+ const aSec = a.segments[i];
153
+ const bSec = b.segments[i];
154
+ const aDyn = aSec.startsWith('[');
155
+ const bDyn = bSec.startsWith('[');
156
+ if (aDyn !== bDyn) {
157
+ return aDyn ? 1 : -1;
158
+ }
159
+ }
160
+ return b.segments.length - a.segments.length;
161
+ });
162
+ }
163
+
164
+ // Pre-cached static files set for production static serving
165
+ const publicFilesSet = new Set();
166
+
167
+ function scanPublicFiles(dir) {
168
+ if (!fs.existsSync(dir)) return;
169
+ const list = fs.readdirSync(dir);
170
+ list.forEach(file => {
171
+ const filePath = path.join(dir, file);
172
+ const stat = fs.statSync(filePath);
173
+ if (stat && stat.isDirectory()) {
174
+ scanPublicFiles(filePath);
175
+ } else {
176
+ publicFilesSet.add(filePath);
177
+ }
178
+ });
179
+ }
180
+
181
+ if (!isDev && config.features.static) {
182
+ scanPublicFiles(path.join(process.cwd(), "public"));
183
+ }
184
+
84
185
  const server = http.createServer(async (req, res) => {
85
186
  const { url, method } = req;
86
187
  const startTime = Date.now();
@@ -129,6 +230,30 @@ function startServer(port = 3000) {
129
230
  req.query = Object.fromEntries(parsedUrl.searchParams);
130
231
  req.path = parsedUrl.pathname;
131
232
 
233
+ // Feature 1: Auto-parsed Cookies
234
+ req.cookies = {};
235
+ if (req.headers.cookie) {
236
+ req.headers.cookie.split(';').forEach(cookie => {
237
+ const parts = cookie.split('=');
238
+ req.cookies[parts.shift().trim()] = decodeURI(parts.join('='));
239
+ });
240
+ }
241
+
242
+ // Feature 2: res.sendFile helper
243
+ res.sendFile = (filePath) => {
244
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(process.cwd(), filePath);
245
+ if (fs.existsSync(absolutePath)) {
246
+ return fs.createReadStream(absolutePath).pipe(res);
247
+ }
248
+ res.status(404).json({ error: "File not found" });
249
+ };
250
+
251
+ // Feature 3: res.redirect helper
252
+ res.redirect = (redirectUrl, status = 302) => {
253
+ res.writeHead(status, { Location: redirectUrl });
254
+ res.end();
255
+ };
256
+
132
257
  // Apply Decorators
133
258
  Object.entries(resDecorators).forEach(([name, fn]) => { res[name] = fn.bind(res); });
134
259
  Object.entries(reqDecorators).forEach(([name, fn]) => { req[name] = fn.bind(req); });
@@ -191,9 +316,15 @@ function startServer(port = 3000) {
191
316
  });
192
317
  };
193
318
 
194
- const parsedData = await parseBody();
195
- req.body = parsedData.body;
196
- req.files = parsedData.files;
319
+ // Conditional body parsed execution (optimization)
320
+ if (['GET', 'HEAD', 'OPTIONS'].includes(method)) {
321
+ req.body = null;
322
+ req.files = [];
323
+ } else {
324
+ const parsedData = await parseBody();
325
+ req.body = parsedData.body;
326
+ req.files = parsedData.files;
327
+ }
197
328
 
198
329
  // Handle OPTIONS requests automatically for CORS if requested
199
330
  if (method === 'OPTIONS') {
@@ -205,6 +336,45 @@ function startServer(port = 3000) {
205
336
  req.params = {};
206
337
  const cleanPath = req.path === "/" ? "/index" : req.path;
207
338
 
339
+ // Feature 4: Built-in Rate Limiting
340
+ if (config.features.rateLimiting) {
341
+ if (!global.rateLimitStore) global.rateLimitStore = {};
342
+ const ip = req.socket.remoteAddress || 'unknown';
343
+ const now = Date.now();
344
+
345
+ const rlConfig = typeof config.features.rateLimiting === 'object' ? config.features.rateLimiting : { max: 100, windowMs: 60000 };
346
+
347
+ if (!global.rateLimitStore[ip] || now > global.rateLimitStore[ip].resetTime) {
348
+ global.rateLimitStore[ip] = { count: 1, resetTime: now + rlConfig.windowMs };
349
+ } else {
350
+ global.rateLimitStore[ip].count++;
351
+ }
352
+
353
+ if (global.rateLimitStore[ip].count > rlConfig.max) {
354
+ return res.status(429).json({ error: "Too Many Requests", message: "Rate limit exceeded. Try again later." });
355
+ }
356
+ }
357
+
358
+ // Feature 5: Static File Serving (optimized via Set lookup in production)
359
+ if (config.features.static && method === 'GET') {
360
+ const publicDir = path.join(process.cwd(), "public");
361
+ const publicPath = path.join(publicDir, cleanPath === "/index" ? "/" : cleanPath);
362
+
363
+ let isStaticFile = false;
364
+ if (!isDev) {
365
+ isStaticFile = publicFilesSet.has(publicPath);
366
+ } else {
367
+ isStaticFile = fs.existsSync(publicDir) && publicPath.startsWith(publicDir) && fs.existsSync(publicPath) && fs.statSync(publicPath).isFile();
368
+ }
369
+
370
+ if (isStaticFile) {
371
+ const ext = path.extname(publicPath);
372
+ const mimeTypes = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.svg': 'image/svg+xml' };
373
+ res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
374
+ return fs.createReadStream(publicPath).pipe(res);
375
+ }
376
+ }
377
+
208
378
  // 10. Enhanced DX: Dev Dashboard
209
379
  if (config.features.dashboard && cleanPath === '/__zerra') {
210
380
  const getRoutes = (dir, base = '') => {
@@ -223,7 +393,7 @@ function startServer(port = 3000) {
223
393
  // Try to extract schema for playground presets
224
394
  let schema = null;
225
395
  try {
226
- const mod = jiti(filePath);
396
+ const mod = getModule(filePath);
227
397
  schema = mod.schema || (mod.default && mod.default.schema);
228
398
  } catch (e) {}
229
399
 
@@ -371,7 +541,7 @@ function startServer(port = 3000) {
371
541
  <input type="text" id="body-${r.path}" value='${sampleBody}' placeholder='{"key": "value"}' style="flex-grow: 1; padding: 4px 10px; border-radius: 4px; border: 1px solid var(--border); font-family: monospace; font-size: 0.8rem;">
372
542
  <button onclick="testRoute('${r.path}')" style="background: var(--primary); color: #fff; border: none; padding: 5px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.8rem;">SEND</button>
373
543
  </div>
374
-
544
+
375
545
  <div id="res-${r.path}" style="display: none; background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 6px; font-family: monospace; font-size: 0.85rem; overflow-x: auto; margin-top: 10px; position: relative;">
376
546
  <div id="status-${r.path}" style="position: absolute; top: 8px; right: 8px; font-size: 0.7rem; font-weight: bold;"></div>
377
547
  <pre style="margin: 0;"></pre>
@@ -380,7 +550,7 @@ function startServer(port = 3000) {
380
550
  `}).join('') : '<div style="color:#999">No routes found in /api</div>'}
381
551
  </div>
382
552
  </section>
383
-
553
+
384
554
  <section>
385
555
  <h2>āš™ļø Features</h2>
386
556
  <div style="display: flex; flex-direction: column; gap: 12px;">
@@ -393,7 +563,7 @@ function startServer(port = 3000) {
393
563
  </div>
394
564
  </section>
395
565
  </div>
396
-
566
+
397
567
  <section style="margin-bottom: 20px;">
398
568
  <h2>šŸ“Š Recent Activity</h2>
399
569
  <table>
@@ -422,7 +592,7 @@ function startServer(port = 3000) {
422
592
  </tbody>
423
593
  </table>
424
594
  </section>
425
-
595
+
426
596
  <section>
427
597
  <h2>šŸ” Environment</h2>
428
598
  <div style="display: flex; flex-wrap: wrap; gap: 10px;">
@@ -440,7 +610,7 @@ function startServer(port = 3000) {
440
610
  </div>
441
611
  </section>
442
612
  </main>
443
-
613
+
444
614
  <script>
445
615
  async function testRoute(path) {
446
616
  const method = document.getElementById('method-' + path).value;
@@ -474,12 +644,12 @@ function startServer(port = 3000) {
474
644
  pre.innerText = err.message;
475
645
  }
476
646
  }
477
-
647
+
478
648
  // Soft refresh every 5 seconds
479
649
  let refreshTimeout = setTimeout(() => {
480
650
  window.location.reload();
481
651
  }, 5000);
482
-
652
+
483
653
  // Pause refresh if user is interacting with playground
484
654
  document.addEventListener('mousedown', () => {
485
655
  clearTimeout(refreshTimeout);
@@ -491,75 +661,107 @@ function startServer(port = 3000) {
491
661
  `);
492
662
  }
493
663
 
494
- let filePath = path.join(apiDir, `${cleanPath}.js`);
495
- if (!fs.existsSync(filePath)) {
496
- filePath = path.join(apiDir, `${cleanPath}.ts`);
497
- }
664
+ let filePath = null;
665
+ let middlewarePaths = [];
498
666
 
499
- // 6. Enhanced DX: Dynamic Routing ([id].js)
500
- if (config.features.dynamicRouting && !fs.existsSync(filePath)) {
501
- const parts = cleanPath.split('/').filter(Boolean);
502
- let currentDir = apiDir;
503
- let matchedFile = null;
667
+ if (!isDev) {
668
+ // 1. Production Mode: Ultra-fast O(1)/O(N) in-memory route lookup
669
+ const reqSegments = cleanPath.split('/').filter(Boolean);
670
+ for (const route of routeTable) {
671
+ if (route.segments.length !== reqSegments.length) continue;
504
672
 
505
- for (let i = 0; i < parts.length; i++) {
506
- const part = parts[i];
507
- const isLast = i === parts.length - 1;
508
-
509
- if (fs.existsSync(currentDir)) {
510
- const files = fs.readdirSync(currentDir);
511
-
512
- // Look for exact match first
513
- let match = files.find(f => isLast ? f === `${part}.js` : f === part && fs.statSync(path.join(currentDir, f)).isDirectory());
673
+ let isMatch = true;
674
+ const tempParams = {};
675
+
676
+ for (let i = 0; i < route.segments.length; i++) {
677
+ const routeSec = route.segments[i];
678
+ const reqSec = reqSegments[i];
679
+
680
+ if (routeSec.startsWith('[') && routeSec.endsWith(']')) {
681
+ const paramName = routeSec.slice(1, -1);
682
+ tempParams[paramName] = reqSec;
683
+ } else if (routeSec !== reqSec) {
684
+ isMatch = false;
685
+ break;
686
+ }
687
+ }
688
+
689
+ if (isMatch) {
690
+ filePath = route.filePath;
691
+ req.params = tempParams;
692
+ middlewarePaths = route.middlewarePaths;
693
+ break;
694
+ }
695
+ }
696
+ } else {
697
+ // 2. Development Mode: Dynamic Filesystem Walk for instant hot-reloading
698
+ filePath = path.join(apiDir, `${cleanPath}.js`);
699
+ if (!fs.existsSync(filePath)) {
700
+ filePath = path.join(apiDir, `${cleanPath}.ts`);
701
+ }
702
+
703
+ if (config.features.dynamicRouting && !fs.existsSync(filePath)) {
704
+ const parts = cleanPath.split('/').filter(Boolean);
705
+ let currentDir = apiDir;
706
+ let matchedFile = null;
707
+
708
+ for (let i = 0; i < parts.length; i++) {
709
+ const part = parts[i];
710
+ const isLast = i === parts.length - 1;
514
711
 
515
- // Look for dynamic parameter [param]
516
- if (!match) {
517
- match = files.find(f => isLast ? (f.startsWith('[') && (f.endsWith('].js') || f.endsWith('].ts'))) : (f.startsWith('[') && f.endsWith(']') && fs.statSync(path.join(currentDir, f)).isDirectory()));
518
- if (match) {
519
- const paramName = isLast ? match.slice(1, match.lastIndexOf('].')) : match.slice(1, -1);
520
- req.params[paramName] = part;
712
+ if (fs.existsSync(currentDir)) {
713
+ const files = fs.readdirSync(currentDir);
714
+
715
+ // Look for exact match first
716
+ let match = files.find(f => isLast ? f === `${part}.js` : f === part && fs.statSync(path.join(currentDir, f)).isDirectory());
717
+
718
+ // Look for dynamic parameter [param]
719
+ if (!match) {
720
+ match = files.find(f => isLast ? (f.startsWith('[') && (f.endsWith('].js') || f.endsWith('].ts'))) : (f.startsWith('[') && f.endsWith(']') && fs.statSync(path.join(currentDir, f)).isDirectory()));
721
+ if (match) {
722
+ const paramName = isLast ? match.slice(1, match.lastIndexOf('].')) : match.slice(1, -1);
723
+ req.params[paramName] = part;
724
+ }
521
725
  }
522
- }
523
726
 
524
- if (match) {
525
- if (isLast) {
526
- matchedFile = path.join(currentDir, match);
727
+ if (match) {
728
+ if (isLast) {
729
+ matchedFile = path.join(currentDir, match);
730
+ } else {
731
+ currentDir = path.join(currentDir, match);
732
+ }
527
733
  } else {
528
- currentDir = path.join(currentDir, match);
734
+ break;
529
735
  }
530
736
  } else {
531
737
  break;
532
738
  }
533
- } else {
534
- break;
739
+ }
740
+
741
+ if (matchedFile) {
742
+ filePath = matchedFile;
535
743
  }
536
744
  }
537
-
538
- if (matchedFile) {
539
- filePath = matchedFile;
540
- }
541
- }
542
745
 
543
- if (fs.existsSync(filePath)) {
544
- try {
545
- // 7. Enhanced DX: Middleware (_middleware.js)
746
+ // Populate middleware chain for dev mode dynamically
747
+ if (filePath && fs.existsSync(filePath) && config.features.middleware) {
546
748
  const targetDir = path.dirname(filePath);
547
749
  let currentPath = targetDir;
548
- const middlewarePaths = [];
549
-
550
- if (config.features.middleware) {
551
- while (currentPath.length >= apiDir.length && currentPath.startsWith(apiDir)) {
552
- let mwPath = path.join(currentPath, '_middleware.js');
553
- if (!fs.existsSync(mwPath)) mwPath = path.join(currentPath, '_middleware.ts');
554
-
555
- if (fs.existsSync(mwPath)) {
556
- middlewarePaths.unshift(mwPath); // Run top-down
557
- }
558
- if (currentPath === apiDir) break;
559
- currentPath = path.dirname(currentPath);
750
+ while (currentPath.length >= apiDir.length && currentPath.startsWith(apiDir)) {
751
+ let mwPath = path.join(currentPath, '_middleware.js');
752
+ if (!fs.existsSync(mwPath)) mwPath = path.join(currentPath, '_middleware.ts');
753
+
754
+ if (fs.existsSync(mwPath)) {
755
+ middlewarePaths.unshift(mwPath);
560
756
  }
757
+ if (currentPath === apiDir) break;
758
+ currentPath = path.dirname(currentPath);
561
759
  }
760
+ }
761
+ }
562
762
 
763
+ if (filePath && fs.existsSync(filePath)) {
764
+ try {
563
765
  let middlewareIndex = 0;
564
766
  let responseSent = false;
565
767
 
@@ -575,8 +777,7 @@ function startServer(port = 3000) {
575
777
 
576
778
  if (middlewareIndex < middlewarePaths.length) {
577
779
  const mwPath = middlewarePaths[middlewareIndex++];
578
- delete require.cache[require.resolve(mwPath)];
579
- const mw = jiti(mwPath);
780
+ const mw = getModule(mwPath);
580
781
  if (typeof mw === 'function' || (mw && typeof mw.default === 'function')) {
581
782
  const actualMw = mw.default || mw;
582
783
  await actualMw(req, res, runNext);
@@ -584,30 +785,61 @@ function startServer(port = 3000) {
584
785
  await runNext();
585
786
  }
586
787
  } else {
587
- delete require.cache[require.resolve(filePath)];
588
- const handler = jiti(filePath);
788
+ const handler = getModule(filePath);
589
789
 
590
790
  if (typeof handler === "function" || (handler && typeof handler.default === "function")) {
591
791
  const actualHandler = handler.default || handler;
592
792
 
593
- // 9. Enhanced DX: Input Validation
594
- const schema = handler.schema;
595
- if (config.features.validation && schema) {
596
- if (!req.body || typeof req.body !== 'object') {
597
- return res.status(400).json({
598
- error: "Validation Failed",
599
- details: ["Request body is required for this route."]
600
- });
793
+ // 9. Enhanced DX: Input Validation (Zod Support)
794
+ const schemaDef = handler.schema;
795
+ if (config.features.validation && schemaDef) {
796
+ let validationErrors = [];
797
+
798
+ const validateZod = (zodSchema, data) => {
799
+ const result = zodSchema.safeParse(data);
800
+ if (!result.success) {
801
+ return { error: true, details: result.error.errors };
802
+ }
803
+ return { error: false, data: result.data };
804
+ };
805
+
806
+ if (schemaDef.safeParse) {
807
+ // Direct Zod schema for body
808
+ const result = validateZod(schemaDef, req.body);
809
+ if (result.error) validationErrors.push(...result.details);
810
+ else req.body = result.data;
811
+ } else if (schemaDef.body || schemaDef.query || schemaDef.params) {
812
+ // Advanced schema object: { body: ZodSchema, query: ZodSchema }
813
+ if (schemaDef.body && schemaDef.body.safeParse) {
814
+ const res = validateZod(schemaDef.body, req.body);
815
+ if (res.error) validationErrors.push({ target: 'body', errors: res.details });
816
+ else req.body = res.data;
817
+ }
818
+ if (schemaDef.query && schemaDef.query.safeParse) {
819
+ const res = validateZod(schemaDef.query, req.query);
820
+ if (res.error) validationErrors.push({ target: 'query', errors: res.details });
821
+ else req.query = res.data;
822
+ }
823
+ if (schemaDef.params && schemaDef.params.safeParse) {
824
+ const res = validateZod(schemaDef.params, req.params);
825
+ if (res.error) validationErrors.push({ target: 'params', errors: res.details });
826
+ else req.params = res.data;
827
+ }
828
+ } else {
829
+ // Fallback to legacy basic typeof validation
830
+ if (!req.body || typeof req.body !== 'object') {
831
+ validationErrors.push("Request body is required for this route.");
832
+ } else {
833
+ for (const [key, type] of Object.entries(schemaDef)) {
834
+ if (typeof req.body[key] !== type) {
835
+ validationErrors.push(`Expected '${key}' to be '${type}', got '${typeof req.body[key]}'`);
836
+ }
837
+ }
838
+ }
601
839
  }
602
840
 
603
- const errors = [];
604
- for (const [key, type] of Object.entries(schema)) {
605
- if (typeof req.body[key] !== type) {
606
- errors.push(`Expected '${key}' to be '${type}', got '${typeof req.body[key]}'`);
607
- }
608
- }
609
- if (errors.length > 0) {
610
- return res.status(400).json({ error: "Validation Failed", details: errors });
841
+ if (validationErrors.length > 0) {
842
+ return res.status(400).json({ error: "Validation Failed", details: validationErrors });
611
843
  }
612
844
  }
613
845
 
@@ -636,8 +868,7 @@ function startServer(port = 3000) {
636
868
  const errorHandlerPath = path.join(apiDir, '_error.js');
637
869
  if (fs.existsSync(errorHandlerPath)) {
638
870
  try {
639
- delete require.cache[require.resolve(errorHandlerPath)];
640
- const errorHandler = jiti(errorHandlerPath);
871
+ const errorHandler = getModule(errorHandlerPath);
641
872
  const actualErrorHandler = errorHandler.default || errorHandler;
642
873
  if (typeof actualErrorHandler === 'function') {
643
874
  return await actualErrorHandler(err, req, res);
@@ -668,6 +899,30 @@ function startServer(port = 3000) {
668
899
  server.listen(port, () => {
669
900
  console.log(`\nšŸš€ Zerra Engine started on http://localhost:${port}`);
670
901
  console.log(`šŸ“ Mapping routes from: ${apiDir}\n`);
902
+
903
+ // Feature 6: Built-in Cron Job Scheduler
904
+ if (config.features.cron) {
905
+ try {
906
+ const nodeCron = require('node-cron');
907
+ const jobsDir = path.join(process.cwd(), "jobs");
908
+ if (fs.existsSync(jobsDir)) {
909
+ const jobFiles = fs.readdirSync(jobsDir).filter(f => f.endsWith('.js') || f.endsWith('.ts'));
910
+ jobFiles.forEach(file => {
911
+ const filePath = path.join(jobsDir, file);
912
+ const job = getModule(filePath);
913
+ const schedule = job.schedule || (job.default && job.default.schedule);
914
+ const task = job.task || job.default || (job.default && job.default.task);
915
+
916
+ if (schedule && typeof task === 'function') {
917
+ nodeCron.schedule(schedule, task);
918
+ console.log(`ā° Scheduled job: ${file} [${schedule}]`);
919
+ }
920
+ });
921
+ }
922
+ } catch (e) {
923
+ console.warn("āš ļø Failed to initialize Cron Jobs. Make sure 'node-cron' is installed.");
924
+ }
925
+ }
671
926
  });
672
927
  }
673
928
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zerra-core",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -12,6 +12,10 @@
12
12
  "type": "commonjs",
13
13
  "dependencies": {
14
14
  "busboy": "^1.6.0",
15
- "jiti": "^2.6.1"
15
+ "jiti": "^2.6.1",
16
+ "node-cron": "^4.2.1"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node-cron": "^3.0.11"
16
20
  }
17
21
  }