naidejs 1.2.0 → 1.4.0

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.
package/src/parser.js CHANGED
@@ -61,7 +61,16 @@ export class Parser {
61
61
  expect(type) {
62
62
  const tok = this.advance();
63
63
  if (tok.type !== type) {
64
- throw this.error(`Expected ${type} but got ${tok.type} ('${tok.value}')`, tok);
64
+ const hints = {
65
+ COLON: "Missing ':' — blocks (if, fn, server, etc.) need a colon at the end",
66
+ INDENT: "Expected an indented block — check your indentation (use 2 spaces)",
67
+ RPAREN: "Missing closing ')'",
68
+ RBRACKET: "Missing closing ']'",
69
+ RBRACE: "Missing closing '}'",
70
+ IDENT: "Expected an identifier (variable/function name)",
71
+ };
72
+ const hint = hints[type] ? `\n Hint: ${hints[type]}` : '';
73
+ throw this.error(`Expected ${type} but got ${tok.type} ('${tok.value}')${hint}`, tok);
65
74
  }
66
75
  return tok;
67
76
  }
@@ -81,18 +90,28 @@ export class Parser {
81
90
  return types.includes(this.peek().type);
82
91
  }
83
92
 
93
+ isIdentLike(type) {
94
+ return type === T.IDENT || TYPE_TOKENS.has(type) ||
95
+ type === T.LOG || type === T.DB || type === T.GET ||
96
+ type === T.POST || type === T.PUT || type === T.DEL ||
97
+ type === T.MATCH || type === T.ON || type === T.NEW ||
98
+ type === T.FROM || type === T.AS || type === T.SELF ||
99
+ type === T.SCHEMA || type === T.CRUD || type === T.AUTH ||
100
+ type === T.CORS || type === T.LIMIT || type === T.ENV ||
101
+ type === T.EVERY || type === T.WATCH || type === T.STATIC ||
102
+ type === T.WS || type === T.GROUP || type === T.COOKIE ||
103
+ type === T.NOT || type === T.AND || type === T.OR ||
104
+ type === T.IN || type === T.BREAK || type === T.CONTINUE ||
105
+ type === T.THROW || type === T.MUT || type === T.PUB ||
106
+ type === T.UPLOAD || type === T.SESSION || type === T.VIEW ||
107
+ type === T.SSE || type === T.CACHE || type === T.PATCH || type === T.MID ||
108
+ type === T.VALIDATE || type === T.TEST || type === T.ASSERT ||
109
+ type === T.QUEUE || type === T.JOB || type === T.OPENAPI;
110
+ }
111
+
84
112
  expectPropertyName() {
85
113
  const tok = this.advance();
86
- if (tok.type === T.IDENT || TYPE_TOKENS.has(tok.type) ||
87
- tok.type === T.LOG || tok.type === T.DB || tok.type === T.GET ||
88
- tok.type === T.POST || tok.type === T.PUT || tok.type === T.DEL ||
89
- tok.type === T.MATCH || tok.type === T.ON || tok.type === T.NEW ||
90
- tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF ||
91
- tok.type === T.SCHEMA || tok.type === T.CRUD || tok.type === T.AUTH ||
92
- tok.type === T.CORS || tok.type === T.LIMIT || tok.type === T.ENV ||
93
- tok.type === T.EVERY || tok.type === T.WATCH ||
94
- tok.type === T.STATIC || tok.type === T.WS ||
95
- tok.type === T.GROUP || tok.type === T.COOKIE) {
114
+ if (this.isIdentLike(tok.type)) {
96
115
  return tok.value;
97
116
  }
98
117
  throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
@@ -186,6 +205,18 @@ export class Parser {
186
205
  if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
187
206
  this.advance();
188
207
  return new ASTNode('CookieDecl');
208
+ case T.VALIDATE:
209
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
210
+ return this.parseValidate();
211
+ case T.TEST:
212
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
213
+ return this.parseTest();
214
+ case T.ASSERT:
215
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
216
+ return this.parseAssert();
217
+ case T.QUEUE:
218
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
219
+ return this.parseQueue();
189
220
  default:
190
221
  if (TYPE_TOKENS.has(tok.type)) {
191
222
  return this.parseTypedVariable();
@@ -237,7 +268,11 @@ export class Parser {
237
268
 
238
269
  parseFunction(isAsync, isPublic) {
239
270
  this.advance(); // fn or fn.async
240
- const name = this.expect(T.IDENT).value;
271
+ const tok = this.advance();
272
+ if (!this.isIdentLike(tok.type)) {
273
+ throw this.error(`Expected function name but got ${tok.type} ('${tok.value}')`, tok);
274
+ }
275
+ const name = tok.value;
241
276
  this.expect(T.LPAREN);
242
277
  const params = this.parseFnParams();
243
278
  this.expect(T.RPAREN);
@@ -328,6 +363,14 @@ export class Parser {
328
363
  const body = this.parseExpression();
329
364
  return new ASTNode('ReturnStatus', { method, statusCode, body });
330
365
  }
366
+ if (method === 'render') {
367
+ const template = this.parseExpression();
368
+ let data = null;
369
+ if (!this.at(T.NEWLINE) && !this.at(T.EOF) && !this.at(T.DEDENT)) {
370
+ data = this.parseExpression();
371
+ }
372
+ return new ASTNode('ReturnRender', { template, data });
373
+ }
331
374
  const value = this.parseExpression();
332
375
  return new ASTNode('ReturnMethod', { method, value });
333
376
  }
@@ -499,10 +542,15 @@ export class Parser {
499
542
  this.skipNewlines();
500
543
 
501
544
  while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
502
- if (this.atAny(T.GET, T.POST, T.PUT, T.DEL)) {
545
+ if (this.atAny(T.GET, T.POST, T.PUT, T.DEL, T.PATCH)) {
503
546
  routes.push(this.parseRoute());
504
547
  } else if (this.at(T.MID)) {
505
- middleware.push(this.parseMiddleware());
548
+ const mid = this.parseMiddleware();
549
+ if (mid.type === 'MiddlewareRef') {
550
+ routes.push(mid);
551
+ } else {
552
+ middleware.push(mid);
553
+ }
506
554
  } else if (this.at(T.CRUD)) {
507
555
  routes.push(this.parseCrud());
508
556
  } else if (this.at(T.AUTH)) {
@@ -520,6 +568,22 @@ export class Parser {
520
568
  } else if (this.at(T.COOKIE)) {
521
569
  this.advance();
522
570
  routes.push(new ASTNode('CookieDecl'));
571
+ } else if (this.at(T.UPLOAD)) {
572
+ routes.push(this.parseUpload());
573
+ } else if (this.at(T.SESSION)) {
574
+ routes.push(this.parseSession());
575
+ } else if (this.at(T.VIEW)) {
576
+ routes.push(this.parseView());
577
+ } else if (this.at(T.SSE)) {
578
+ routes.push(this.parseSse());
579
+ } else if (this.at(T.CACHE)) {
580
+ routes.push(this.parseCache());
581
+ } else if (this.at(T.VALIDATE)) {
582
+ routes.push(this.parseValidate());
583
+ } else if (this.at(T.OPENAPI)) {
584
+ routes.push(this.parseOpenapi());
585
+ } else if (this.at(T.QUEUE)) {
586
+ routes.push(this.parseQueue());
523
587
  } else if (this.at(T.IDENT) && this.peek().value === 'error') {
524
588
  routes.push(this.parseErrorHandler());
525
589
  } else {
@@ -559,16 +623,23 @@ export class Parser {
559
623
  parseMiddleware() {
560
624
  this.advance(); // mid
561
625
  const name = this.expect(T.IDENT).value;
562
- this.expect(T.LPAREN);
563
- const params = [];
564
- while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
565
- params.push(this.expect(T.IDENT).value);
566
- this.match(T.COMMA);
626
+ if (this.at(T.LPAREN)) {
627
+ this.advance();
628
+ const params = [];
629
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
630
+ params.push(this.expect(T.IDENT).value);
631
+ this.match(T.COMMA);
632
+ }
633
+ this.expect(T.RPAREN);
634
+ this.expect(T.COLON);
635
+ const body = this.parseBlock();
636
+ return new ASTNode('Middleware', { name, params, body });
567
637
  }
568
- this.expect(T.RPAREN);
569
- this.expect(T.COLON);
570
- const body = this.parseBlock();
571
- return new ASTNode('Middleware', { name, params, body });
638
+ let path = null;
639
+ if (this.at(T.STRING)) {
640
+ path = this.parseString();
641
+ }
642
+ return new ASTNode('MiddlewareRef', { name, path });
572
643
  }
573
644
 
574
645
  parseModel(isPublic = false) {
@@ -726,6 +797,18 @@ export class Parser {
726
797
  }
727
798
 
728
799
  parseTernary() {
800
+ if (this.at(T.IF)) {
801
+ const savedPos = this.pos;
802
+ this.advance();
803
+ const condition = this.parseNullish();
804
+ if (this.match(T.THEN)) {
805
+ const consequent = this.parseNullish();
806
+ this.expect(T.ELSE);
807
+ const alternate = this.parseNullish();
808
+ return new ASTNode('Ternary', { condition, consequent, alternate });
809
+ }
810
+ this.pos = savedPos;
811
+ }
729
812
  let expr = this.parseNullish();
730
813
  if (this.at(T.IF)) {
731
814
  const savedPos = this.pos;
@@ -906,6 +989,11 @@ export class Parser {
906
989
  case T.SCHEMA: case T.CRUD: case T.AUTH: case T.CORS:
907
990
  case T.LIMIT: case T.ENV: case T.EVERY: case T.WATCH:
908
991
  case T.STATIC: case T.WS: case T.GROUP: case T.COOKIE:
992
+ case T.UPLOAD: case T.SESSION: case T.VIEW: case T.SSE:
993
+ case T.CACHE: case T.PATCH: case T.MID:
994
+ case T.VALIDATE: case T.TEST: case T.ASSERT:
995
+ case T.QUEUE: case T.JOB: case T.OPENAPI:
996
+ case T.FROM: case T.AS: case T.IN:
909
997
  this.advance();
910
998
  return new ASTNode('Identifier', { name: tok.value });
911
999
 
@@ -1344,7 +1432,7 @@ export class Parser {
1344
1432
  this.skipNewlines();
1345
1433
 
1346
1434
  while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1347
- if (this.atAny(T.GET, T.POST, T.PUT, T.DEL)) {
1435
+ if (this.atAny(T.GET, T.POST, T.PUT, T.DEL, T.PATCH)) {
1348
1436
  routes.push(this.parseRoute());
1349
1437
  } else if (this.at(T.MID)) {
1350
1438
  routes.push(this.parseMiddleware());
@@ -1379,4 +1467,108 @@ export class Parser {
1379
1467
  const body = this.parseBlock();
1380
1468
  return new ASTNode('ErrorHandler', { params, body });
1381
1469
  }
1470
+
1471
+ parseUpload() {
1472
+ this.advance(); // upload
1473
+ const path = this.parseString();
1474
+ const fieldName = this.parseString();
1475
+ let params = [];
1476
+ if (this.match(T.LPAREN)) {
1477
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1478
+ params.push(this.expect(T.IDENT).value);
1479
+ this.match(T.COMMA);
1480
+ }
1481
+ this.expect(T.RPAREN);
1482
+ }
1483
+ this.expect(T.COLON);
1484
+ const body = this.parseBlock();
1485
+ return new ASTNode('UploadDecl', { path, fieldName, params, body });
1486
+ }
1487
+
1488
+ parseSession() {
1489
+ this.advance(); // session
1490
+ const secret = this.parseExpression();
1491
+ return new ASTNode('SessionDecl', { secret });
1492
+ }
1493
+
1494
+ parseView() {
1495
+ this.advance(); // view
1496
+ const dir = this.parseString();
1497
+ return new ASTNode('ViewDecl', { dir });
1498
+ }
1499
+
1500
+ parseSse() {
1501
+ this.advance(); // sse
1502
+ const path = this.parseString();
1503
+ return new ASTNode('SseDecl', { path });
1504
+ }
1505
+
1506
+ parseCache() {
1507
+ this.advance(); // cache
1508
+ const path = this.parseString();
1509
+ const duration = this.parseExpression();
1510
+ return new ASTNode('CacheDecl', { path, duration });
1511
+ }
1512
+
1513
+ parseValidate() {
1514
+ this.advance(); // validate
1515
+ const path = this.parseString();
1516
+ const schemaName = this.expect(T.IDENT).value;
1517
+ return new ASTNode('ValidateDecl', { path, schemaName });
1518
+ }
1519
+
1520
+ parseTest() {
1521
+ this.advance(); // test
1522
+ const name = this.parseExpression();
1523
+ this.expect(T.COLON);
1524
+ const body = this.parseBlock();
1525
+ return new ASTNode('TestDecl', { name, body });
1526
+ }
1527
+
1528
+ parseAssert() {
1529
+ this.advance(); // assert
1530
+ const expr = this.parseExpression();
1531
+ return new ASTNode('AssertStmt', { expr });
1532
+ }
1533
+
1534
+ parseQueue() {
1535
+ this.advance(); // queue
1536
+ const name = this.expect(T.IDENT).value;
1537
+ this.expect(T.COLON);
1538
+ this.skipNewlines();
1539
+ this.expect(T.INDENT);
1540
+
1541
+ const jobs = [];
1542
+ this.skipNewlines();
1543
+
1544
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1545
+ if (this.at(T.JOB)) {
1546
+ this.advance();
1547
+ const jobName = this.parseString();
1548
+ let params = [];
1549
+ if (this.match(T.LPAREN)) {
1550
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1551
+ params.push(this.expect(T.IDENT).value);
1552
+ this.match(T.COMMA);
1553
+ }
1554
+ this.expect(T.RPAREN);
1555
+ }
1556
+ this.expect(T.COLON);
1557
+ const body = this.parseBlock();
1558
+ jobs.push({ name: jobName, params, body });
1559
+ } else {
1560
+ this.advance();
1561
+ }
1562
+ this.skipNewlines();
1563
+ }
1564
+ this.match(T.DEDENT);
1565
+
1566
+ return new ASTNode('QueueDecl', { name, jobs });
1567
+ }
1568
+
1569
+ parseOpenapi() {
1570
+ this.advance(); // openapi
1571
+ const path = this.parseString();
1572
+ return new ASTNode('OpenapiDecl', { path });
1573
+ }
1382
1574
  }
package/src/preprocess.js CHANGED
@@ -115,6 +115,8 @@ export function preprocess(source) {
115
115
  out = 'ret.text ' + transformContent(rest.slice(3));
116
116
  } else if (rest.startsWith('.f ')) {
117
117
  out = 'ret.file ' + transformContent(rest.slice(3));
118
+ } else if (rest.startsWith('.v ')) {
119
+ out = 'ret.render ' + transformContent(rest.slice(3));
118
120
  } else {
119
121
  out = rest ? 'ret ' + transformContent(rest) : 'ret';
120
122
  }
@@ -179,6 +181,7 @@ export function preprocess(source) {
179
181
  else if (first === 'P' && second === '"') { out = transformRoute('post', line.slice(1)); }
180
182
  else if (first === 'U' && second === '"') { out = transformRoute('put', line.slice(1)); }
181
183
  else if (first === 'D' && second === '"') { out = transformRoute('del', line.slice(1)); }
184
+ else if (first === 'X' && second === '"') { out = transformRoute('patch', line.slice(1)); }
182
185
  // ^ model
183
186
  else if (first === '^') {
184
187
  const rest = line.slice(1).trim();
package/src/runtime.js CHANGED
@@ -408,6 +408,264 @@ export function cookieParser() {
408
408
  };
409
409
  }
410
410
 
411
+ // ===== Session Middleware (zero-dep, cookie-based) =====
412
+ export function sessionMiddleware(secret) {
413
+ const store = new Map();
414
+ const maxAge = 86400000;
415
+ return (req, res, next) => {
416
+ const cookies = req.headers.cookie || '';
417
+ const sidMatch = cookies.match(/(?:^|;\s*)naide_sid=([^;]+)/);
418
+ let sid = sidMatch ? sidMatch[1] : null;
419
+ if (!sid || !store.has(sid)) {
420
+ sid = randomUUID();
421
+ store.set(sid, {});
422
+ }
423
+ req.session = store.get(sid);
424
+ req.sessionId = sid;
425
+ req.session.destroy = () => { store.delete(sid); };
426
+ const origWriteHead = res.writeHead;
427
+ res.writeHead = function(...args) {
428
+ res.setHeader('Set-Cookie', `naide_sid=${sid}; HttpOnly; Path=/; Max-Age=${maxAge / 1000}; SameSite=Lax`);
429
+ origWriteHead.apply(res, args);
430
+ };
431
+ for (const [id, data] of store) {
432
+ if (data.__ts && Date.now() - data.__ts > maxAge) store.delete(id);
433
+ }
434
+ req.session.__ts = Date.now();
435
+ next();
436
+ };
437
+ }
438
+
439
+ // ===== Template Renderer (zero-dep) =====
440
+ export function createRenderer(viewsDir) {
441
+ return (name, data = {}) => {
442
+ const filePath = viewsDir.replace(/\/$/, '') + '/' + name + (name.includes('.') ? '' : '.html');
443
+ let template = readFileSync(filePath, 'utf-8');
444
+ template = template.replace(/\{\{\s*each\s+(\w+)\s+in\s+(\w+)\s*\}\}([\s\S]*?)\{\{\s*\/each\s*\}\}/g, (_, item, list, block) => {
445
+ const arr = data[list] || [];
446
+ return arr.map(val => block.replace(new RegExp(`\\{\\{\\s*${item}\\b[^}]*\\}\\}`, 'g'), m => {
447
+ const prop = m.match(/\{\{\s*\w+\.(\w+)\s*\}\}/);
448
+ return prop ? String(val[prop[1]] ?? '') : String(val ?? '');
449
+ })).join('');
450
+ });
451
+ template = template.replace(/\{\{\s*if\s+(\w+)\s*\}\}([\s\S]*?)\{\{\s*\/if\s*\}\}/g, (_, key, block) => data[key] ? block : '');
452
+ for (const [key, value] of Object.entries(data)) {
453
+ template = template.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'g'), String(value ?? ''));
454
+ }
455
+ return template;
456
+ };
457
+ }
458
+
459
+ // ===== Cache Middleware (zero-dep) =====
460
+ export function cacheMiddleware(duration) {
461
+ const maxAge = parseMs(duration);
462
+ const cache = new Map();
463
+ return (req, res, next) => {
464
+ if (req.method !== 'GET') return next();
465
+ const key = req.originalUrl || req.url;
466
+ const cached = cache.get(key);
467
+ if (cached && Date.now() - cached.time < maxAge) {
468
+ res.setHeader('X-Cache', 'HIT');
469
+ const ct = cached.contentType || 'application/json';
470
+ res.setHeader('Content-Type', ct);
471
+ return res.end(cached.body);
472
+ }
473
+ const origEnd = res.end;
474
+ const origJson = res.json ? res.json.bind(res) : null;
475
+ if (origJson) {
476
+ res.json = (data) => {
477
+ cache.set(key, { body: JSON.stringify(data), contentType: 'application/json', time: Date.now() });
478
+ res.setHeader('X-Cache', 'MISS');
479
+ origJson(data);
480
+ };
481
+ }
482
+ const origSend = res.send ? res.send.bind(res) : null;
483
+ if (origSend) {
484
+ res.send = (body) => {
485
+ cache.set(key, { body: String(body), contentType: res.getHeader('content-type'), time: Date.now() });
486
+ res.setHeader('X-Cache', 'MISS');
487
+ origSend(body);
488
+ };
489
+ }
490
+ next();
491
+ };
492
+ }
493
+
494
+ // ===== Upload Middleware (zero-dep multipart parser) =====
495
+ export function uploadMiddleware(fieldName, opts = {}) {
496
+ const maxSize = opts.maxSize || 10 * 1024 * 1024;
497
+ return (req, res, next) => {
498
+ const contentType = req.headers['content-type'] || '';
499
+ if (!contentType.startsWith('multipart/form-data')) return next();
500
+ const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^\s;]+))/);
501
+ if (!boundaryMatch) return next();
502
+ const boundary = boundaryMatch[1] || boundaryMatch[2];
503
+ const chunks = [];
504
+ let size = 0;
505
+ req.on('data', chunk => {
506
+ size += chunk.length;
507
+ if (size > maxSize) { req.destroy(); return res.status(413).json({ error: 'File too large' }); }
508
+ chunks.push(chunk);
509
+ });
510
+ req.on('end', () => {
511
+ try {
512
+ const buffer = Buffer.concat(chunks);
513
+ const { files, fields } = _parseMultipart(buffer, boundary);
514
+ req.files = files;
515
+ req.file = files[fieldName] || files[Object.keys(files)[0]] || null;
516
+ if (!req.body) req.body = {};
517
+ Object.assign(req.body, fields);
518
+ next();
519
+ } catch { res.status(400).json({ error: 'Invalid multipart data' }); }
520
+ });
521
+ req.on('error', () => res.status(400).json({ error: 'Upload failed' }));
522
+ };
523
+ }
524
+
525
+ function _parseMultipart(buffer, boundary) {
526
+ const files = {}, fields = {};
527
+ const delim = Buffer.from(`--${boundary}`);
528
+ let start = buffer.indexOf(delim);
529
+ if (start === -1) return { files, fields };
530
+ start += delim.length + 2;
531
+ const endDelim = Buffer.from(`--${boundary}--`);
532
+ while (start < buffer.length) {
533
+ let end = buffer.indexOf(delim, start);
534
+ if (end === -1) break;
535
+ const part = buffer.slice(start, end - 2);
536
+ const headerEnd = part.indexOf('\r\n\r\n');
537
+ if (headerEnd !== -1) {
538
+ const header = part.slice(0, headerEnd).toString('utf-8');
539
+ const body = part.slice(headerEnd + 4);
540
+ const nameMatch = header.match(/name="([^"]+)"/);
541
+ if (nameMatch) {
542
+ const filenameMatch = header.match(/filename="([^"]+)"/);
543
+ if (filenameMatch) {
544
+ const ctMatch = header.match(/Content-Type:\s*(.+)/i);
545
+ files[nameMatch[1]] = { filename: filenameMatch[1], contentType: ctMatch ? ctMatch[1].trim() : 'application/octet-stream', data: body, size: body.length };
546
+ } else {
547
+ fields[nameMatch[1]] = body.toString('utf-8');
548
+ }
549
+ }
550
+ }
551
+ start = end + delim.length;
552
+ if (buffer.slice(end, end + endDelim.length).equals(endDelim)) break;
553
+ start += 2;
554
+ }
555
+ return { files, fields };
556
+ }
557
+
558
+ // ===== SSE Manager (zero-dep) =====
559
+ export function createSseManager() {
560
+ const clients = new Set();
561
+ return {
562
+ handler() {
563
+ return (req, res) => {
564
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*' });
565
+ res.write(':\n\n');
566
+ const client = { res, id: randomUUID() };
567
+ clients.add(client);
568
+ req.on('close', () => clients.delete(client));
569
+ };
570
+ },
571
+ send(data, event) {
572
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
573
+ for (const client of clients) {
574
+ if (event) client.res.write(`event: ${event}\n`);
575
+ client.res.write(`data: ${payload}\n\n`);
576
+ }
577
+ },
578
+ broadcast(data, event) { this.send(data, event); },
579
+ get count() { return clients.size; }
580
+ };
581
+ }
582
+
583
+ // ===== Validate Middleware =====
584
+ export function validateMiddleware(schema) {
585
+ return (req, res, next) => {
586
+ if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
587
+ const result = schema.validate(req.body || {});
588
+ if (!result.valid) {
589
+ return res.status(400).json({ errors: result.errors });
590
+ }
591
+ req.body = result.data;
592
+ }
593
+ next();
594
+ };
595
+ }
596
+
597
+ // ===== Job Queue (in-memory async) =====
598
+ export function createQueue() {
599
+ const handlers = new Map();
600
+ const pending = [];
601
+ let running = false;
602
+
603
+ async function process() {
604
+ if (running) return;
605
+ running = true;
606
+ while (pending.length > 0) {
607
+ const { name, data, resolve, reject } = pending.shift();
608
+ const handler = handlers.get(name);
609
+ if (handler) {
610
+ try { await handler(data); resolve(); } catch (e) { console.error('[NAIDE Queue]', e.message); reject(e); }
611
+ } else {
612
+ reject(new Error(`No handler for job: ${name}`));
613
+ }
614
+ }
615
+ running = false;
616
+ }
617
+
618
+ return {
619
+ register(name, fn) { handlers.set(name, fn); },
620
+ add(name, data) {
621
+ return new Promise((resolve, reject) => {
622
+ pending.push({ name, data, resolve, reject });
623
+ process();
624
+ });
625
+ },
626
+ get size() { return pending.length; }
627
+ };
628
+ }
629
+
630
+ // ===== OpenAPI Spec Builder =====
631
+ export function buildOpenApiSpec(schemas) {
632
+ const spec = {
633
+ openapi: '3.1.0',
634
+ info: { title: 'API', version: '1.0.0' },
635
+ paths: {},
636
+ components: { schemas: {} }
637
+ };
638
+ for (const schema of schemas) {
639
+ const properties = {};
640
+ const required = [];
641
+ for (const [fieldName, fieldDef] of Object.entries(schema.fields)) {
642
+ const prop = {};
643
+ switch (fieldDef.type) {
644
+ case 'id': prop.type = 'string'; prop.format = 'uuid'; break;
645
+ case 'string': prop.type = 'string'; break;
646
+ case 'integer': prop.type = 'integer'; break;
647
+ case 'number': prop.type = 'number'; break;
648
+ case 'boolean': prop.type = 'boolean'; break;
649
+ case 'timestamp': prop.type = 'string'; prop.format = 'date-time'; break;
650
+ case 'enum':
651
+ prop.type = 'string';
652
+ if (fieldDef.values) prop.enum = fieldDef.values;
653
+ break;
654
+ default: prop.type = 'string';
655
+ }
656
+ if (fieldDef.min !== undefined) prop.minimum = fieldDef.min;
657
+ if (fieldDef.max !== undefined) prop.maximum = fieldDef.max;
658
+ if (fieldDef.email) prop.format = 'email';
659
+ if (fieldDef.url) prop.format = 'uri';
660
+ properties[fieldName] = prop;
661
+ if (fieldDef.required) required.push(fieldName);
662
+ }
663
+ spec.components.schemas[schema.name] = { type: 'object', properties };
664
+ if (required.length > 0) spec.components.schemas[schema.name].required = required;
665
+ }
666
+ return spec;
667
+ }
668
+
411
669
  // ===== Helpers =====
412
670
  function parseMs(str) {
413
671
  if (typeof str === 'number') return str;
package/src/tokens.js CHANGED
@@ -69,6 +69,18 @@ export const T = {
69
69
  WS: 'WS',
70
70
  GROUP: 'GROUP',
71
71
  COOKIE: 'COOKIE',
72
+ UPLOAD: 'UPLOAD',
73
+ SESSION: 'SESSION',
74
+ VIEW: 'VIEW',
75
+ SSE: 'SSE',
76
+ CACHE: 'CACHE',
77
+ PATCH: 'PATCH',
78
+ VALIDATE: 'VALIDATE',
79
+ TEST: 'TEST',
80
+ ASSERT: 'ASSERT',
81
+ QUEUE: 'QUEUE',
82
+ JOB: 'JOB',
83
+ OPENAPI: 'OPENAPI',
72
84
 
73
85
  // Operators
74
86
  ASSIGN: 'ASSIGN',
@@ -166,6 +178,18 @@ export const KEYWORDS = {
166
178
  'ws': T.WS,
167
179
  'group': T.GROUP,
168
180
  'cookie': T.COOKIE,
181
+ 'upload': T.UPLOAD,
182
+ 'session': T.SESSION,
183
+ 'view': T.VIEW,
184
+ 'sse': T.SSE,
185
+ 'cache': T.CACHE,
186
+ 'patch': T.PATCH,
187
+ 'validate': T.VALIDATE,
188
+ 'test': T.TEST,
189
+ 'assert': T.ASSERT,
190
+ 'queue': T.QUEUE,
191
+ 'job': T.JOB,
192
+ 'openapi': T.OPENAPI,
169
193
  'true': T.BOOL,
170
194
  'false': T.BOOL,
171
195
  'null': T.NULL,