naidejs 1.3.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/README.md +382 -229
- package/SPEC-X.nx +23 -1
- package/SPEC.naide +56 -0
- package/bin/naide.js +99 -2
- package/package.json +1 -1
- package/src/generator.js +199 -0
- package/src/parser.js +178 -14
- package/src/preprocess.js +3 -0
- package/src/runtime.js +258 -0
- package/src/tokens.js +24 -0
package/src/parser.js
CHANGED
|
@@ -102,7 +102,11 @@ export class Parser {
|
|
|
102
102
|
type === T.WS || type === T.GROUP || type === T.COOKIE ||
|
|
103
103
|
type === T.NOT || type === T.AND || type === T.OR ||
|
|
104
104
|
type === T.IN || type === T.BREAK || type === T.CONTINUE ||
|
|
105
|
-
type === T.THROW || type === T.MUT || type === T.PUB
|
|
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;
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
expectPropertyName() {
|
|
@@ -201,6 +205,18 @@ export class Parser {
|
|
|
201
205
|
if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
|
|
202
206
|
this.advance();
|
|
203
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();
|
|
204
220
|
default:
|
|
205
221
|
if (TYPE_TOKENS.has(tok.type)) {
|
|
206
222
|
return this.parseTypedVariable();
|
|
@@ -252,7 +268,11 @@ export class Parser {
|
|
|
252
268
|
|
|
253
269
|
parseFunction(isAsync, isPublic) {
|
|
254
270
|
this.advance(); // fn or fn.async
|
|
255
|
-
const
|
|
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;
|
|
256
276
|
this.expect(T.LPAREN);
|
|
257
277
|
const params = this.parseFnParams();
|
|
258
278
|
this.expect(T.RPAREN);
|
|
@@ -343,6 +363,14 @@ export class Parser {
|
|
|
343
363
|
const body = this.parseExpression();
|
|
344
364
|
return new ASTNode('ReturnStatus', { method, statusCode, body });
|
|
345
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
|
+
}
|
|
346
374
|
const value = this.parseExpression();
|
|
347
375
|
return new ASTNode('ReturnMethod', { method, value });
|
|
348
376
|
}
|
|
@@ -514,10 +542,15 @@ export class Parser {
|
|
|
514
542
|
this.skipNewlines();
|
|
515
543
|
|
|
516
544
|
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
517
|
-
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)) {
|
|
518
546
|
routes.push(this.parseRoute());
|
|
519
547
|
} else if (this.at(T.MID)) {
|
|
520
|
-
|
|
548
|
+
const mid = this.parseMiddleware();
|
|
549
|
+
if (mid.type === 'MiddlewareRef') {
|
|
550
|
+
routes.push(mid);
|
|
551
|
+
} else {
|
|
552
|
+
middleware.push(mid);
|
|
553
|
+
}
|
|
521
554
|
} else if (this.at(T.CRUD)) {
|
|
522
555
|
routes.push(this.parseCrud());
|
|
523
556
|
} else if (this.at(T.AUTH)) {
|
|
@@ -535,6 +568,22 @@ export class Parser {
|
|
|
535
568
|
} else if (this.at(T.COOKIE)) {
|
|
536
569
|
this.advance();
|
|
537
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());
|
|
538
587
|
} else if (this.at(T.IDENT) && this.peek().value === 'error') {
|
|
539
588
|
routes.push(this.parseErrorHandler());
|
|
540
589
|
} else {
|
|
@@ -574,16 +623,23 @@ export class Parser {
|
|
|
574
623
|
parseMiddleware() {
|
|
575
624
|
this.advance(); // mid
|
|
576
625
|
const name = this.expect(T.IDENT).value;
|
|
577
|
-
this.
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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 });
|
|
582
637
|
}
|
|
583
|
-
|
|
584
|
-
this.
|
|
585
|
-
|
|
586
|
-
|
|
638
|
+
let path = null;
|
|
639
|
+
if (this.at(T.STRING)) {
|
|
640
|
+
path = this.parseString();
|
|
641
|
+
}
|
|
642
|
+
return new ASTNode('MiddlewareRef', { name, path });
|
|
587
643
|
}
|
|
588
644
|
|
|
589
645
|
parseModel(isPublic = false) {
|
|
@@ -933,6 +989,10 @@ export class Parser {
|
|
|
933
989
|
case T.SCHEMA: case T.CRUD: case T.AUTH: case T.CORS:
|
|
934
990
|
case T.LIMIT: case T.ENV: case T.EVERY: case T.WATCH:
|
|
935
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:
|
|
936
996
|
case T.FROM: case T.AS: case T.IN:
|
|
937
997
|
this.advance();
|
|
938
998
|
return new ASTNode('Identifier', { name: tok.value });
|
|
@@ -1372,7 +1432,7 @@ export class Parser {
|
|
|
1372
1432
|
this.skipNewlines();
|
|
1373
1433
|
|
|
1374
1434
|
while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
|
|
1375
|
-
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)) {
|
|
1376
1436
|
routes.push(this.parseRoute());
|
|
1377
1437
|
} else if (this.at(T.MID)) {
|
|
1378
1438
|
routes.push(this.parseMiddleware());
|
|
@@ -1407,4 +1467,108 @@ export class Parser {
|
|
|
1407
1467
|
const body = this.parseBlock();
|
|
1408
1468
|
return new ASTNode('ErrorHandler', { params, body });
|
|
1409
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
|
+
}
|
|
1410
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,
|