naidejs 1.0.0 → 1.1.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 CHANGED
@@ -9,7 +9,7 @@ Two modes:
9
9
  ## Install
10
10
 
11
11
  ```bash
12
- npm install -g naide
12
+ npm install -g naidejs
13
13
  ```
14
14
 
15
15
  ## Usage
@@ -29,6 +29,117 @@ naide -o app.js app.nx
29
29
  naide --mid app.nx
30
30
  ```
31
31
 
32
+ ## High-Level Features
33
+
34
+ NAIDE includes built-in declarations for common backend patterns. Zero dependencies — the runtime is bundled with the package.
35
+
36
+ ### schema — Data models with validation
37
+
38
+ ```python
39
+ schema User:
40
+ id auto
41
+ name str required min(2) max(50)
42
+ email str required email unique
43
+ age int optional min(0) max(150)
44
+ role enum("admin", "user") default("user")
45
+ joined timestamp auto
46
+ ```
47
+
48
+ Types: `str`, `int`, `num`, `bool`, `auto` (UUID), `timestamp`, `enum(...)`
49
+ Modifiers: `required`, `optional`, `min(n)`, `max(n)`, `email`, `url`, `unique`, `auto`, `default(val)`
50
+
51
+ ### crud — Auto-generate REST endpoints
52
+
53
+ ```python
54
+ server app port 3000:
55
+ crud "/api/users" User
56
+ ```
57
+
58
+ Generates GET (list + by ID), POST, PUT, DELETE routes with validation.
59
+
60
+ ### auth — JWT authentication
61
+
62
+ ```python
63
+ server app port 3000:
64
+ auth JWT_SECRET:
65
+ protect "/api/*"
66
+ public "/api/auth/*"
67
+ ```
68
+
69
+ Built-in JWT sign/verify with no external dependencies.
70
+
71
+ ### cors — CORS middleware
72
+
73
+ ```python
74
+ server app port 3000:
75
+ cors "*"
76
+ # or: cors ["localhost:3000", "myapp.com"]
77
+ ```
78
+
79
+ ### limit — Rate limiting
80
+
81
+ ```python
82
+ server app port 3000:
83
+ limit "/api/*" 100 "1m"
84
+ ```
85
+
86
+ ### env — Environment variables with validation
87
+
88
+ ```python
89
+ env:
90
+ PORT int default(3000)
91
+ JWT_SECRET str required
92
+ DB_URL str default("sqlite:data.db")
93
+ ```
94
+
95
+ Variables become constants available throughout the file.
96
+
97
+ ### every — Scheduled tasks
98
+
99
+ ```python
100
+ every "5m":
101
+ log "cleanup running"
102
+ ```
103
+
104
+ Intervals: `"30s"`, `"5m"`, `"1h"`, `"1d"`
105
+
106
+ ### watch — React to events
107
+
108
+ ```python
109
+ watch User.create (event):
110
+ log "new user: {event.data.name}"
111
+ ```
112
+
113
+ Automatically connected to `crud` events.
114
+
115
+ ### Full example
116
+
117
+ ```python
118
+ env:
119
+ PORT int default(3000)
120
+ JWT_SECRET str required
121
+
122
+ schema User:
123
+ id auto
124
+ name str required min(2) max(50)
125
+ email str required email
126
+
127
+ server app port PORT:
128
+ cors "*"
129
+ auth JWT_SECRET:
130
+ protect "/api/*"
131
+ public "/api/auth/*"
132
+ limit "/api/*" 100 "1m"
133
+ crud "/api/users" User
134
+ get "/health":
135
+ ret {status: "ok"}
136
+
137
+ watch User.create (event):
138
+ log "new user: {event.data.name}"
139
+ ```
140
+
141
+ This generates a complete Express API with validation, auth, CORS, rate limiting, and CRUD — from 20 lines.
142
+
32
143
  ## NAIDE syntax (.naide)
33
144
 
34
145
  ```python
@@ -37,7 +148,7 @@ str name = "World"
37
148
  int count = 3
38
149
  mut int counter = 0
39
150
 
40
- # Functions — one way only
151
+ # Functions
41
152
  fn greet(str who) -> str:
42
153
  ret "Hello, {who}!"
43
154
 
@@ -118,8 +229,12 @@ f greet(s:who)s
118
229
  @i<0..10
119
230
  log i
120
231
 
121
- -- Server
232
+ -- Server with high-level features
122
233
  $app:3000
234
+ cors "*"
235
+ auth SECRET:
236
+ protect "/api/*"
237
+ crud "/api/users" User
123
238
  G"/users"
124
239
  >users
125
240
  P"/users"(req,res)
@@ -156,6 +271,8 @@ $app:3000
156
271
 
157
272
  Types: `s`=str `i`=int `n`=num `b`=bool `l`=list `m`=map `a`=any
158
273
 
274
+ High-level: `schema`, `crud`, `auth`, `cors`, `limit`, `env`, `every`, `watch` — same syntax in both modes.
275
+
159
276
  ## Why?
160
277
 
161
278
  AI code generation speed depends on:
package/bin/naide.js CHANGED
@@ -61,6 +61,8 @@ if (flags.help || files.length === 0) {
61
61
  process.exit(0);
62
62
  }
63
63
 
64
+ const runtimeUrl = new URL('../src/runtime.js', import.meta.url).href;
65
+
64
66
  for (const file of files) {
65
67
  const filePath = resolve(file);
66
68
  let source;
@@ -81,7 +83,8 @@ for (const file of files) {
81
83
  continue;
82
84
  }
83
85
 
84
- const result = compile(source, { mode });
86
+ const runtimePath = flags.emit || flags.output ? 'naidejs/runtime' : runtimeUrl;
87
+ const result = compile(source, { mode, runtimePath });
85
88
 
86
89
  if (flags.tokens) {
87
90
  console.log(JSON.stringify(result.tokens, null, 2));
@@ -0,0 +1,37 @@
1
+ # Full NAIDE app with high-level features
2
+
3
+ env:
4
+ PORT int default(3000)
5
+ JWT_SECRET str required
6
+
7
+ schema User:
8
+ id auto
9
+ name str required min(2) max(50)
10
+ email str required email
11
+ role str default("user")
12
+
13
+ schema Todo:
14
+ id auto
15
+ title str required min(1)
16
+ done bool default(false)
17
+ created timestamp auto
18
+
19
+ server app port PORT:
20
+ cors "*"
21
+ auth JWT_SECRET:
22
+ protect "/api/*"
23
+ public "/api/auth/*"
24
+ limit "/api/*" 100 "1m"
25
+ crud "/api/users" User
26
+ crud "/api/todos" Todo
27
+ get "/health":
28
+ ret {status: "ok"}
29
+ post "/api/auth/login" (req, res):
30
+ str email = req.body.email
31
+ ret {token: "jwt-token-here"}
32
+
33
+ every "30m":
34
+ log "cleanup running"
35
+
36
+ watch User.create (event):
37
+ log "new user: {event.data.name}"
@@ -0,0 +1,37 @@
1
+ -- Full NAIDE-X app with high-level features
2
+
3
+ env:
4
+ PORT int default(3000)
5
+ JWT_SECRET str required
6
+
7
+ schema User:
8
+ id auto
9
+ name str required min(2) max(50)
10
+ email str required email
11
+ role str default("user")
12
+
13
+ schema Todo:
14
+ id auto
15
+ title str required min(1)
16
+ done bool default(false)
17
+ created timestamp auto
18
+
19
+ $app:PORT
20
+ cors "*"
21
+ auth JWT_SECRET:
22
+ protect "/api/*"
23
+ public "/api/auth/*"
24
+ limit "/api/*" 100 "1m"
25
+ crud "/api/users" User
26
+ crud "/api/todos" Todo
27
+ G"/health"
28
+ >{status:"ok"}
29
+ P"/api/auth/login"(req,res)
30
+ s:email=req.body.email
31
+ >{token:"jwt-token-here"}
32
+
33
+ every "30m":
34
+ log"cleanup running"
35
+
36
+ watch User.create (event):
37
+ log"new user: {event.data.name}"
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "naidejs",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "NAIDE - Node AI Development Environment. A language optimized for AI code generation that transpiles to Node.js. Standard mode (~40% fewer tokens) and X mode (~80% fewer tokens).",
5
5
  "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./runtime": "./src/runtime.js"
9
+ },
6
10
  "bin": {
7
11
  "naide": "bin/naide.js"
8
12
  },
package/src/generator.js CHANGED
@@ -1,13 +1,39 @@
1
1
  export class Generator {
2
- constructor() {
2
+ constructor(options = {}) {
3
3
  this.indent = 0;
4
4
  this.output = [];
5
5
  this.usesExpress = false;
6
6
  this.models = new Map();
7
+ this.runtimeImports = new Set();
8
+ this.schemas = new Map();
9
+ this.needsEventBus = false;
10
+ this.runtimePath = options.runtimePath || 'naidejs/runtime';
7
11
  }
8
12
 
9
13
  generate(ast) {
10
14
  this.visitProgram(ast);
15
+
16
+ const preamble = [];
17
+
18
+ if (this.needsEventBus) {
19
+ this.runtimeImports.add('createEventBus');
20
+ }
21
+
22
+ if (this.runtimeImports.size > 0) {
23
+ const imports = [...this.runtimeImports].join(', ');
24
+ preamble.push(`import { ${imports} } from '${this.runtimePath}';`);
25
+ preamble.push('');
26
+ }
27
+
28
+ if (this.needsEventBus) {
29
+ preamble.push('const __eventBus = createEventBus();');
30
+ preamble.push('');
31
+ }
32
+
33
+ if (preamble.length > 0) {
34
+ this.output.unshift(...preamble);
35
+ }
36
+
11
37
  return this.output.join('\n');
12
38
  }
13
39
 
@@ -51,6 +77,14 @@ export class Generator {
51
77
  case 'ExprStatement': this.emit(this.expr(node.expression) + ';'); return;
52
78
  case 'DbConnect': return this.visitDbConnect(node);
53
79
  case 'AwaitAll': return this.visitAwaitAllStatement(node);
80
+ case 'SchemaDecl': return this.visitSchema(node);
81
+ case 'CrudDecl': return this.visitCrudTopLevel(node);
82
+ case 'AuthDecl': return this.visitAuthTopLevel(node);
83
+ case 'CorsDecl': return this.visitCorsTopLevel(node);
84
+ case 'LimitDecl': return this.visitLimitTopLevel(node);
85
+ case 'EnvDecl': return this.visitEnv(node);
86
+ case 'EveryDecl': return this.visitEvery(node);
87
+ case 'WatchDecl': return this.visitWatch(node);
54
88
  default:
55
89
  this.emit(`/* unknown: ${node.type} */`);
56
90
  }
@@ -111,7 +145,6 @@ export class Generator {
111
145
  const target = this.expr(node.target);
112
146
  const value = this.expr(node.value);
113
147
 
114
- // Destructuring assignment
115
148
  if (node.target.type === 'Array') {
116
149
  const names = node.target.elements.map(e => this.expr(e)).join(', ');
117
150
  this.emit(`const [${names}] = ${value};`);
@@ -225,22 +258,27 @@ export class Generator {
225
258
  this.emit(`${node.name}.use(express.json());`);
226
259
  this.emitRaw('');
227
260
 
228
- // Middleware
229
261
  for (const mid of node.middleware) {
230
262
  this.emit(`${node.name}.use(${this.generateMiddleware(mid)});`);
231
263
  this.emitRaw('');
232
264
  }
233
265
 
234
- // Routes
235
- for (const route of node.routes) {
236
- if (route.type === 'Route') {
237
- this.visitRoute(node.name, route);
266
+ for (const child of node.routes) {
267
+ if (child.type === 'Route') {
268
+ this.visitRoute(node.name, child);
269
+ } else if (child.type === 'CrudDecl') {
270
+ this.visitCrud(node.name, child);
271
+ } else if (child.type === 'AuthDecl') {
272
+ this.visitAuth(node.name, child);
273
+ } else if (child.type === 'CorsDecl') {
274
+ this.visitCors(node.name, child);
275
+ } else if (child.type === 'LimitDecl') {
276
+ this.visitLimit(node.name, child);
238
277
  } else {
239
- this.visitStatement(route);
278
+ this.visitStatement(child);
240
279
  }
241
280
  }
242
281
 
243
- // Listen
244
282
  const port = node.port ? this.expr(node.port) : '3000';
245
283
  this.emitRaw('');
246
284
  this.emit(`${node.name}.listen(${port}, () => {`);
@@ -255,17 +293,14 @@ export class Generator {
255
293
  const path = this.stringValue(route.path);
256
294
  const params = route.params.length > 0 ? route.params.join(', ') : 'req, res';
257
295
 
258
- // Check if body uses await
259
296
  const needsAsync = this.bodyUsesAwait(route.body);
260
297
  const asyncPrefix = needsAsync ? 'async ' : '';
261
298
 
262
299
  this.emit(`${appName}.${method}(${path}, ${asyncPrefix}(${params}) => {`);
263
300
  this.indent++;
264
301
 
265
- // If params don't include res, inject it
266
302
  const hasRes = params.includes('res');
267
303
 
268
- // Transform body: last expression with ret becomes res.json
269
304
  for (let i = 0; i < route.body.length; i++) {
270
305
  const stmt = route.body[i];
271
306
  if (stmt.type === 'Return' && stmt.value !== null) {
@@ -326,7 +361,6 @@ export class Generator {
326
361
  this.emitRaw('');
327
362
  }
328
363
 
329
- // Methods
330
364
  for (const method of node.methods) {
331
365
  const async = method.isAsync ? 'async ' : '';
332
366
  const params = method.params.map(p => {
@@ -353,8 +387,6 @@ export class Generator {
353
387
 
354
388
  visitOn(node) {
355
389
  const event = this.expr(node.event);
356
- // Split event into object and event name
357
- // e.g., process.exit -> process.on('exit', ...)
358
390
  if (node.event.type === 'MemberAccess') {
359
391
  const obj = this.expr(node.event.object);
360
392
  const evt = node.event.property;
@@ -376,7 +408,6 @@ export class Generator {
376
408
 
377
409
  visitThrow(node) {
378
410
  const value = this.expr(node.value);
379
- // If it's a string, wrap in Error
380
411
  if (node.value.type === 'String') {
381
412
  this.emit(`throw new Error(${value});`);
382
413
  } else {
@@ -393,7 +424,207 @@ export class Generator {
393
424
  this.emit(`await Promise.all([${exprs}]);`);
394
425
  }
395
426
 
396
- // Expression generation
427
+ // ===== New high-level features =====
428
+
429
+ visitSchema(node) {
430
+ this.runtimeImports.add('createSchema');
431
+ this.runtimeImports.add('createStore');
432
+
433
+ this.emit(`const ${node.name}Schema = createSchema('${node.name}', {`);
434
+ this.indent++;
435
+
436
+ for (const field of node.fields) {
437
+ const props = [];
438
+
439
+ switch (field.type) {
440
+ case 'auto':
441
+ props.push("type: 'id'", 'auto: true');
442
+ break;
443
+ case 'timestamp':
444
+ props.push("type: 'timestamp'");
445
+ break;
446
+ case 'str':
447
+ props.push("type: 'string'");
448
+ break;
449
+ case 'int':
450
+ props.push("type: 'integer'");
451
+ break;
452
+ case 'num':
453
+ props.push("type: 'number'");
454
+ break;
455
+ case 'bool':
456
+ props.push("type: 'boolean'");
457
+ break;
458
+ case 'enum':
459
+ if (field.enumValues) {
460
+ const vals = field.enumValues.map(v => this.expr(v)).join(', ');
461
+ props.push("type: 'enum'", `values: [${vals}]`);
462
+ } else {
463
+ props.push("type: 'enum'");
464
+ }
465
+ break;
466
+ default:
467
+ props.push(`type: '${field.type}'`);
468
+ break;
469
+ }
470
+
471
+ for (const mod of field.modifiers) {
472
+ switch (mod.name) {
473
+ case 'required': props.push('required: true'); break;
474
+ case 'optional': props.push('required: false'); break;
475
+ case 'unique': props.push('unique: true'); break;
476
+ case 'email': props.push('email: true'); break;
477
+ case 'url': props.push('url: true'); break;
478
+ case 'auto': props.push('auto: true'); break;
479
+ case 'min': props.push(`min: ${this.expr(mod.args[0])}`); break;
480
+ case 'max': props.push(`max: ${this.expr(mod.args[0])}`); break;
481
+ case 'default': props.push(`default: ${this.expr(mod.args[0])}`); break;
482
+ }
483
+ }
484
+
485
+ this.emit(`${field.name}: { ${props.join(', ')} },`);
486
+ }
487
+
488
+ this.indent--;
489
+ this.emit('});');
490
+ this.emit(`const ${node.name}Store = createStore(${node.name}Schema);`);
491
+ this.emitRaw('');
492
+
493
+ this.schemas.set(node.name, node);
494
+ }
495
+
496
+ visitCrud(appName, node) {
497
+ this.runtimeImports.add('registerCrud');
498
+ this.needsEventBus = true;
499
+
500
+ const path = this.stringValue(node.path);
501
+ this.emit(`registerCrud(${appName}, ${path}, ${node.schemaName}Schema, ${node.schemaName}Store, __eventBus);`);
502
+ this.emitRaw('');
503
+ }
504
+
505
+ visitCrudTopLevel(node) {
506
+ this.visitCrud('app', node);
507
+ }
508
+
509
+ visitAuth(appName, node) {
510
+ this.runtimeImports.add('jwtAuth');
511
+
512
+ const secret = this.expr(node.secret);
513
+ const options = [];
514
+ if (node.publicPaths.length > 0) {
515
+ const paths = node.publicPaths.map(p => this.stringValue(p)).join(', ');
516
+ options.push(`public: [${paths}]`);
517
+ }
518
+ const optStr = options.length > 0 ? `, { ${options.join(', ')} }` : '';
519
+
520
+ if (node.protectedPaths.length > 0) {
521
+ const path = this.stringValue(node.protectedPaths[0]);
522
+ this.emit(`${appName}.use(${path}, jwtAuth(${secret}${optStr}));`);
523
+ } else {
524
+ this.emit(`${appName}.use(jwtAuth(${secret}${optStr}));`);
525
+ }
526
+ this.emitRaw('');
527
+ }
528
+
529
+ visitAuthTopLevel(node) {
530
+ this.visitAuth('app', node);
531
+ }
532
+
533
+ visitCors(appName, node) {
534
+ this.runtimeImports.add('corsMiddleware');
535
+
536
+ const origins = this.expr(node.origins);
537
+ if (node.origins.type === 'String') {
538
+ this.emit(`${appName}.use(corsMiddleware([${origins}]));`);
539
+ } else {
540
+ this.emit(`${appName}.use(corsMiddleware(${origins}));`);
541
+ }
542
+ this.emitRaw('');
543
+ }
544
+
545
+ visitCorsTopLevel(node) {
546
+ this.visitCors('app', node);
547
+ }
548
+
549
+ visitLimit(appName, node) {
550
+ this.runtimeImports.add('rateLimit');
551
+
552
+ const path = this.stringValue(node.path);
553
+ const max = this.expr(node.max);
554
+ const window = this.expr(node.window);
555
+ this.emit(`${appName}.use(${path}, rateLimit(${max}, ${window}));`);
556
+ this.emitRaw('');
557
+ }
558
+
559
+ visitLimitTopLevel(node) {
560
+ this.visitLimit('app', node);
561
+ }
562
+
563
+ visitEnv(node) {
564
+ this.runtimeImports.add('loadEnv');
565
+
566
+ const names = node.vars.map(v => v.name);
567
+ this.emit(`const { ${names.join(', ')} } = loadEnv({`);
568
+ this.indent++;
569
+
570
+ for (const v of node.vars) {
571
+ const props = [];
572
+
573
+ switch (v.type) {
574
+ case 'str': props.push("type: 'string'"); break;
575
+ case 'int': props.push("type: 'integer'"); break;
576
+ case 'num': props.push("type: 'number'"); break;
577
+ case 'bool': props.push("type: 'boolean'"); break;
578
+ default: props.push(`type: '${v.type}'`); break;
579
+ }
580
+
581
+ for (const mod of v.modifiers) {
582
+ switch (mod.name) {
583
+ case 'required': props.push('required: true'); break;
584
+ case 'default': props.push(`default: ${this.expr(mod.args[0])}`); break;
585
+ }
586
+ }
587
+
588
+ this.emit(`${v.name}: { ${props.join(', ')} },`);
589
+ }
590
+
591
+ this.indent--;
592
+ this.emit('});');
593
+ this.emitRaw('');
594
+ }
595
+
596
+ visitEvery(node) {
597
+ this.runtimeImports.add('scheduleEvery');
598
+
599
+ const interval = this.expr(node.interval);
600
+ const needsAsync = this.bodyUsesAwait(node.body);
601
+ const asyncPrefix = needsAsync ? 'async ' : '';
602
+
603
+ this.emit(`scheduleEvery(${interval}, ${asyncPrefix}() => {`);
604
+ this.indent++;
605
+ for (const stmt of node.body) this.visitStatement(stmt);
606
+ this.indent--;
607
+ this.emit('});');
608
+ this.emitRaw('');
609
+ }
610
+
611
+ visitWatch(node) {
612
+ this.needsEventBus = true;
613
+
614
+ const params = node.params.length > 0 ? node.params.join(', ') : 'event';
615
+ const needsAsync = this.bodyUsesAwait(node.body);
616
+ const asyncPrefix = needsAsync ? 'async ' : '';
617
+
618
+ this.emit(`__eventBus.on('${node.eventName}', ${asyncPrefix}(${params}) => {`);
619
+ this.indent++;
620
+ for (const stmt of node.body) this.visitStatement(stmt);
621
+ this.indent--;
622
+ this.emit('});');
623
+ this.emitRaw('');
624
+ }
625
+
626
+ // ===== Expression generation =====
627
+
397
628
  expr(node) {
398
629
  if (!node) return 'undefined';
399
630
 
package/src/index.js CHANGED
@@ -3,7 +3,7 @@ import { Parser } from './parser.js';
3
3
  import { Generator } from './generator.js';
4
4
  import { preprocess } from './preprocess.js';
5
5
 
6
- export function compile(source, { mode = 'naide' } = {}) {
6
+ export function compile(source, { mode = 'naide', runtimePath } = {}) {
7
7
  if (mode === 'x') {
8
8
  source = preprocess(source);
9
9
  }
@@ -11,7 +11,7 @@ export function compile(source, { mode = 'naide' } = {}) {
11
11
  const tokens = lexer.tokenize();
12
12
  const parser = new Parser(tokens);
13
13
  const ast = parser.parse();
14
- const generator = new Generator();
14
+ const generator = new Generator({ runtimePath });
15
15
  const js = generator.generate(ast);
16
16
  return { js, ast, tokens, naide: mode === 'x' ? source : null };
17
17
  }
package/src/parser.js CHANGED
@@ -91,7 +91,10 @@ export class Parser {
91
91
  tok.type === T.LOG || tok.type === T.DB || tok.type === T.GET ||
92
92
  tok.type === T.POST || tok.type === T.PUT || tok.type === T.DEL ||
93
93
  tok.type === T.MATCH || tok.type === T.ON || tok.type === T.NEW ||
94
- tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF) {
94
+ tok.type === T.FROM || tok.type === T.AS || tok.type === T.SELF ||
95
+ tok.type === T.SCHEMA || tok.type === T.CRUD || tok.type === T.AUTH ||
96
+ tok.type === T.CORS || tok.type === T.LIMIT || tok.type === T.ENV ||
97
+ tok.type === T.EVERY || tok.type === T.WATCH) {
95
98
  return tok.value;
96
99
  }
97
100
  throw this.error(`Expected property name but got ${tok.type} ('${tok.value}')`, tok);
@@ -148,6 +151,28 @@ export class Parser {
148
151
  case T.DB: return this.parseDbStatement();
149
152
  case T.AWAIT: return this.parseAwaitStatement();
150
153
  case T.AWAIT_ALL: return this.parseAwaitAll();
154
+ case T.SCHEMA: return this.parseSchema();
155
+ case T.CRUD:
156
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
157
+ return this.parseCrud();
158
+ case T.AUTH:
159
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
160
+ return this.parseAuth();
161
+ case T.CORS:
162
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
163
+ return this.parseCors();
164
+ case T.LIMIT:
165
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
166
+ return this.parseLimit();
167
+ case T.ENV:
168
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
169
+ return this.parseEnv();
170
+ case T.EVERY:
171
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
172
+ return this.parseEvery();
173
+ case T.WATCH:
174
+ if (this.peek(1).type === T.DOT) return this.parseExpressionStatement();
175
+ return this.parseWatch();
151
176
  default:
152
177
  if (TYPE_TOKENS.has(tok.type)) {
153
178
  return this.parseTypedVariable();
@@ -497,8 +522,15 @@ export class Parser {
497
522
  routes.push(this.parseRoute());
498
523
  } else if (this.at(T.MID)) {
499
524
  middleware.push(this.parseMiddleware());
525
+ } else if (this.at(T.CRUD)) {
526
+ routes.push(this.parseCrud());
527
+ } else if (this.at(T.AUTH)) {
528
+ routes.push(this.parseAuth());
529
+ } else if (this.at(T.CORS)) {
530
+ routes.push(this.parseCors());
531
+ } else if (this.at(T.LIMIT)) {
532
+ routes.push(this.parseLimit());
500
533
  } else {
501
- // generic statement in server block
502
534
  routes.push(this.parseStatement());
503
535
  }
504
536
  this.skipNewlines();
@@ -894,6 +926,11 @@ export class Parser {
894
926
  this.advance();
895
927
  return new ASTNode('Identifier', { name: tok.value });
896
928
 
929
+ case T.SCHEMA: case T.CRUD: case T.AUTH: case T.CORS:
930
+ case T.LIMIT: case T.ENV: case T.EVERY: case T.WATCH:
931
+ this.advance();
932
+ return new ASTNode('Identifier', { name: tok.value });
933
+
897
934
  case T.DB:
898
935
  return this.parseDbExpression();
899
936
 
@@ -1076,4 +1113,217 @@ export class Parser {
1076
1113
  const body = this.parseBlock();
1077
1114
  return new ASTNode('Lambda', { params, returnType, body, isAsync });
1078
1115
  }
1116
+
1117
+ // schema User:
1118
+ // id auto
1119
+ // name str required min(2) max(50)
1120
+ // email str required email unique
1121
+ parseSchema() {
1122
+ this.advance(); // schema
1123
+ const name = this.expect(T.IDENT).value;
1124
+ this.expect(T.COLON);
1125
+ this.skipNewlines();
1126
+ this.expect(T.INDENT);
1127
+
1128
+ const fields = [];
1129
+ this.skipNewlines();
1130
+
1131
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1132
+ fields.push(this.parseSchemaField());
1133
+ this.skipNewlines();
1134
+ }
1135
+ this.match(T.DEDENT);
1136
+
1137
+ return new ASTNode('SchemaDecl', { name, fields });
1138
+ }
1139
+
1140
+ parseSchemaField() {
1141
+ const name = this.expect(T.IDENT).value;
1142
+
1143
+ let fieldType;
1144
+ let enumValues = null;
1145
+
1146
+ if (TYPE_TOKENS.has(this.peek().type)) {
1147
+ fieldType = this.advance().value;
1148
+ } else if (this.at(T.IDENT)) {
1149
+ fieldType = this.advance().value;
1150
+ if (fieldType === 'enum' && this.at(T.LPAREN)) {
1151
+ this.advance();
1152
+ enumValues = [];
1153
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1154
+ enumValues.push(this.parseExpression());
1155
+ this.match(T.COMMA);
1156
+ }
1157
+ this.expect(T.RPAREN);
1158
+ }
1159
+ } else {
1160
+ throw this.error('Expected type in schema field');
1161
+ }
1162
+
1163
+ const modifiers = [];
1164
+ while (!this.at(T.NEWLINE) && !this.at(T.DEDENT) && !this.at(T.EOF)) {
1165
+ if (this.at(T.IDENT) || this.at(T.IDENT)) {
1166
+ const modName = this.advance().value;
1167
+ if (this.at(T.LPAREN)) {
1168
+ this.advance();
1169
+ const args = [];
1170
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1171
+ args.push(this.parseExpression());
1172
+ this.match(T.COMMA);
1173
+ }
1174
+ this.expect(T.RPAREN);
1175
+ modifiers.push({ name: modName, args });
1176
+ } else {
1177
+ modifiers.push({ name: modName, args: [] });
1178
+ }
1179
+ } else {
1180
+ break;
1181
+ }
1182
+ }
1183
+
1184
+ return { name, type: fieldType, enumValues, modifiers };
1185
+ }
1186
+
1187
+ // crud "/api/users" User
1188
+ parseCrud() {
1189
+ this.advance(); // crud
1190
+ const path = this.parseString();
1191
+ const schemaName = this.expect(T.IDENT).value;
1192
+ return new ASTNode('CrudDecl', { path, schemaName });
1193
+ }
1194
+
1195
+ // auth SECRET:
1196
+ // protect "/api/*"
1197
+ // public "/api/auth/*"
1198
+ parseAuth() {
1199
+ this.advance(); // auth
1200
+ const secret = this.parseExpression();
1201
+
1202
+ const protectedPaths = [];
1203
+ const publicPaths = [];
1204
+
1205
+ if (this.match(T.COLON)) {
1206
+ this.skipNewlines();
1207
+ this.expect(T.INDENT);
1208
+ this.skipNewlines();
1209
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1210
+ if (this.at(T.IDENT) && this.peek().value === 'protect') {
1211
+ this.advance();
1212
+ protectedPaths.push(this.parseString());
1213
+ } else if (this.at(T.IDENT) && this.peek().value === 'public') {
1214
+ this.advance();
1215
+ publicPaths.push(this.parseString());
1216
+ } else {
1217
+ this.advance();
1218
+ }
1219
+ this.skipNewlines();
1220
+ }
1221
+ this.match(T.DEDENT);
1222
+ }
1223
+
1224
+ return new ASTNode('AuthDecl', { secret, protectedPaths, publicPaths });
1225
+ }
1226
+
1227
+ // cors "*"
1228
+ // cors ["origin1", "origin2"]
1229
+ parseCors() {
1230
+ this.advance(); // cors
1231
+ const origins = this.parseExpression();
1232
+ return new ASTNode('CorsDecl', { origins });
1233
+ }
1234
+
1235
+ // limit "/api/*" 100 "1m"
1236
+ parseLimit() {
1237
+ this.advance(); // limit
1238
+ const path = this.parseString();
1239
+ const max = this.parseExpression();
1240
+ const window = this.parseExpression();
1241
+ return new ASTNode('LimitDecl', { path, max, window });
1242
+ }
1243
+
1244
+ // env:
1245
+ // PORT int default(3000)
1246
+ // JWT_SECRET str required
1247
+ parseEnv() {
1248
+ this.advance(); // env
1249
+ this.expect(T.COLON);
1250
+ this.skipNewlines();
1251
+ this.expect(T.INDENT);
1252
+
1253
+ const vars = [];
1254
+ this.skipNewlines();
1255
+
1256
+ while (!this.at(T.DEDENT) && !this.at(T.EOF)) {
1257
+ vars.push(this.parseEnvField());
1258
+ this.skipNewlines();
1259
+ }
1260
+ this.match(T.DEDENT);
1261
+
1262
+ return new ASTNode('EnvDecl', { vars });
1263
+ }
1264
+
1265
+ parseEnvField() {
1266
+ const name = this.expect(T.IDENT).value;
1267
+
1268
+ let fieldType = 'string';
1269
+ if (TYPE_TOKENS.has(this.peek().type)) {
1270
+ fieldType = this.advance().value;
1271
+ } else if (this.at(T.IDENT) && ['string', 'number', 'integer', 'boolean'].includes(this.peek().value)) {
1272
+ fieldType = this.advance().value;
1273
+ }
1274
+
1275
+ const modifiers = [];
1276
+ while (!this.at(T.NEWLINE) && !this.at(T.DEDENT) && !this.at(T.EOF)) {
1277
+ if (this.at(T.IDENT)) {
1278
+ const modName = this.advance().value;
1279
+ if (this.at(T.LPAREN)) {
1280
+ this.advance();
1281
+ const args = [];
1282
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1283
+ args.push(this.parseExpression());
1284
+ this.match(T.COMMA);
1285
+ }
1286
+ this.expect(T.RPAREN);
1287
+ modifiers.push({ name: modName, args });
1288
+ } else {
1289
+ modifiers.push({ name: modName, args: [] });
1290
+ }
1291
+ } else {
1292
+ break;
1293
+ }
1294
+ }
1295
+
1296
+ return { name, type: fieldType, modifiers };
1297
+ }
1298
+
1299
+ // every "5m":
1300
+ // log "tick"
1301
+ parseEvery() {
1302
+ this.advance(); // every
1303
+ const interval = this.parseExpression();
1304
+ this.expect(T.COLON);
1305
+ const body = this.parseBlock();
1306
+ return new ASTNode('EveryDecl', { interval, body });
1307
+ }
1308
+
1309
+ // watch User.create (event):
1310
+ // log event
1311
+ parseWatch() {
1312
+ this.advance(); // watch
1313
+ let eventName = this.expect(T.IDENT).value;
1314
+ while (this.match(T.DOT)) {
1315
+ eventName += '.' + this.expectPropertyName();
1316
+ }
1317
+ let params = [];
1318
+ if (this.match(T.LPAREN)) {
1319
+ while (!this.at(T.RPAREN) && !this.at(T.EOF)) {
1320
+ params.push(this.expect(T.IDENT).value);
1321
+ this.match(T.COMMA);
1322
+ }
1323
+ this.expect(T.RPAREN);
1324
+ }
1325
+ this.expect(T.COLON);
1326
+ const body = this.parseBlock();
1327
+ return new ASTNode('WatchDecl', { eventName, params, body });
1328
+ }
1079
1329
  }
package/src/runtime.js ADDED
@@ -0,0 +1,267 @@
1
+ import { createHmac, randomUUID, timingSafeEqual } from 'crypto';
2
+
3
+ // ===== Schema + Validation =====
4
+ export function createSchema(name, fieldDefs) {
5
+ const schema = {
6
+ name,
7
+ fields: fieldDefs,
8
+ validate(data) {
9
+ const errors = [];
10
+ const result = {};
11
+ for (const [field, rules] of Object.entries(fieldDefs)) {
12
+ let val = data[field];
13
+ if (rules.auto && rules.type === 'id' && val === undefined) { result[field] = randomUUID(); continue; }
14
+ if (rules.auto && rules.type === 'timestamp' && val === undefined) { result[field] = new Date().toISOString(); continue; }
15
+ if (val === undefined && rules.default !== undefined) val = rules.default;
16
+ if (rules.required && (val === undefined || val === null || val === '')) { errors.push(`${field} is required`); continue; }
17
+ if (val === undefined || val === null) continue;
18
+ if (rules.type === 'string') {
19
+ if (typeof val !== 'string') { errors.push(`${field} must be a string`); continue; }
20
+ if (rules.min !== undefined && val.length < rules.min) errors.push(`${field} must be at least ${rules.min} characters`);
21
+ if (rules.max !== undefined && val.length > rules.max) errors.push(`${field} must be at most ${rules.max} characters`);
22
+ if (rules.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val)) errors.push(`${field} must be a valid email`);
23
+ if (rules.url && !/^https?:\/\/.+/.test(val)) errors.push(`${field} must be a valid URL`);
24
+ if (rules.match && !rules.match.test(val)) errors.push(`${field} format is invalid`);
25
+ }
26
+ if (rules.type === 'number' || rules.type === 'integer') {
27
+ const n = Number(val);
28
+ if (isNaN(n)) { errors.push(`${field} must be a number`); continue; }
29
+ if (rules.type === 'integer' && !Number.isInteger(n)) errors.push(`${field} must be an integer`);
30
+ if (rules.min !== undefined && n < rules.min) errors.push(`${field} must be at least ${rules.min}`);
31
+ if (rules.max !== undefined && n > rules.max) errors.push(`${field} must be at most ${rules.max}`);
32
+ val = n;
33
+ }
34
+ if (rules.type === 'boolean') val = Boolean(val);
35
+ if (rules.type === 'enum' && !rules.values.includes(val)) errors.push(`${field} must be one of: ${rules.values.join(', ')}`);
36
+ result[field] = val;
37
+ }
38
+ return errors.length > 0 ? { valid: false, errors } : { valid: true, data: result };
39
+ },
40
+ defaults() {
41
+ const d = {};
42
+ for (const [field, rules] of Object.entries(fieldDefs)) {
43
+ if (rules.default !== undefined) d[field] = rules.default;
44
+ if (rules.auto && rules.type === 'id') d[field] = randomUUID();
45
+ if (rules.auto && rules.type === 'timestamp') d[field] = new Date().toISOString();
46
+ }
47
+ return d;
48
+ }
49
+ };
50
+ return schema;
51
+ }
52
+
53
+ // ===== In-Memory Store =====
54
+ export function createStore(schema) {
55
+ const items = new Map();
56
+ let _idField = null;
57
+ for (const [f, r] of Object.entries(schema.fields)) {
58
+ if (r.auto && r.type === 'id') { _idField = f; break; }
59
+ }
60
+ const idField = _idField || 'id';
61
+
62
+ return {
63
+ getAll() { return [...items.values()]; },
64
+ getById(id) { return items.get(String(id)) || null; },
65
+ count() { return items.size; },
66
+ create(data) {
67
+ const { valid, errors, data: validated } = schema.validate(data);
68
+ if (!valid) return { error: errors };
69
+ const id = validated[idField] || randomUUID();
70
+ validated[idField] = id;
71
+ items.set(String(id), validated);
72
+ return validated;
73
+ },
74
+ update(id, data) {
75
+ const existing = items.get(String(id));
76
+ if (!existing) return null;
77
+ const merged = { ...existing, ...data, [idField]: existing[idField] };
78
+ items.set(String(id), merged);
79
+ return merged;
80
+ },
81
+ delete(id) {
82
+ return items.delete(String(id));
83
+ },
84
+ where(conditions) {
85
+ return [...items.values()].filter(item => {
86
+ for (const [k, v] of Object.entries(conditions)) {
87
+ if (item[k] !== v) return false;
88
+ }
89
+ return true;
90
+ });
91
+ },
92
+ clear() { items.clear(); }
93
+ };
94
+ }
95
+
96
+ // ===== CRUD Route Registration =====
97
+ export function registerCrud(app, basePath, schema, store, eventBus) {
98
+ app.get(basePath, (_req, res) => {
99
+ res.json(store.getAll());
100
+ });
101
+
102
+ app.get(`${basePath}/:id`, (req, res) => {
103
+ const item = store.getById(req.params.id);
104
+ if (!item) return res.status(404).json({ error: `${schema.name} not found` });
105
+ res.json(item);
106
+ });
107
+
108
+ app.post(basePath, (req, res) => {
109
+ const result = store.create(req.body);
110
+ if (result.error) return res.status(400).json({ errors: result.error });
111
+ if (eventBus) eventBus.emit(`${schema.name}.create`, result);
112
+ res.status(201).json(result);
113
+ });
114
+
115
+ app.put(`${basePath}/:id`, (req, res) => {
116
+ const item = store.getById(req.params.id);
117
+ if (!item) return res.status(404).json({ error: `${schema.name} not found` });
118
+ const updated = store.update(req.params.id, req.body);
119
+ if (eventBus) eventBus.emit(`${schema.name}.update`, updated);
120
+ res.json(updated);
121
+ });
122
+
123
+ app.delete(`${basePath}/:id`, (req, res) => {
124
+ const item = store.getById(req.params.id);
125
+ if (!item) return res.status(404).json({ error: `${schema.name} not found` });
126
+ store.delete(req.params.id);
127
+ if (eventBus) eventBus.emit(`${schema.name}.delete`, { id: req.params.id });
128
+ res.json({ deleted: true });
129
+ });
130
+ }
131
+
132
+ // ===== JWT Auth =====
133
+ function base64url(buf) {
134
+ return Buffer.from(buf).toString('base64url');
135
+ }
136
+
137
+ export function jwtSign(payload, secret, expiresIn = '24h') {
138
+ const header = base64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
139
+ const ms = typeof expiresIn === 'number' ? expiresIn : parseMs(expiresIn);
140
+ const body = base64url(JSON.stringify({ ...payload, iat: Math.floor(Date.now() / 1000), exp: Math.floor((Date.now() + ms) / 1000) }));
141
+ const sig = createHmac('sha256', secret).update(`${header}.${body}`).digest('base64url');
142
+ return `${header}.${body}.${sig}`;
143
+ }
144
+
145
+ export function jwtVerify(token, secret) {
146
+ const parts = token.split('.');
147
+ if (parts.length !== 3) throw new Error('Invalid token');
148
+ const sig = createHmac('sha256', secret).update(`${parts[0]}.${parts[1]}`).digest('base64url');
149
+ const sigBuf = Buffer.from(sig);
150
+ const tokenSigBuf = Buffer.from(parts[2]);
151
+ if (sigBuf.length !== tokenSigBuf.length || !timingSafeEqual(sigBuf, tokenSigBuf)) throw new Error('Invalid signature');
152
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
153
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1000)) throw new Error('Token expired');
154
+ return payload;
155
+ }
156
+
157
+ export function jwtAuth(secret, options = {}) {
158
+ const publicPaths = (options.public || []).map(p => new RegExp('^' + p.replace(/\*/g, '.*') + '$'));
159
+ return (req, res, next) => {
160
+ const isPublic = publicPaths.some(re => re.test(req.path));
161
+ if (isPublic) return next();
162
+ const authHeader = req.headers.authorization;
163
+ if (!authHeader || !authHeader.startsWith('Bearer ')) return res.status(401).json({ error: 'No token provided' });
164
+ try {
165
+ req.user = jwtVerify(authHeader.slice(7), secret);
166
+ next();
167
+ } catch (e) {
168
+ res.status(403).json({ error: e.message });
169
+ }
170
+ };
171
+ }
172
+
173
+ // ===== CORS Middleware =====
174
+ export function corsMiddleware(origins = ['*']) {
175
+ const allowAll = origins.includes('*');
176
+ const originSet = new Set(origins.map(o => o.replace(/^https?:\/\//, '')));
177
+ return (req, res, next) => {
178
+ const reqOrigin = req.headers.origin || '';
179
+ const host = reqOrigin.replace(/^https?:\/\//, '');
180
+ if (allowAll || originSet.has(host)) {
181
+ res.setHeader('Access-Control-Allow-Origin', reqOrigin || '*');
182
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH,OPTIONS');
183
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
184
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
185
+ }
186
+ if (req.method === 'OPTIONS') return res.status(204).end();
187
+ next();
188
+ };
189
+ }
190
+
191
+ // ===== Rate Limiting =====
192
+ export function rateLimit(max, window = '1m') {
193
+ const windowMs = parseMs(window);
194
+ const hits = new Map();
195
+ setInterval(() => hits.clear(), windowMs);
196
+ return (req, res, next) => {
197
+ const key = req.ip;
198
+ const count = (hits.get(key) || 0) + 1;
199
+ hits.set(key, count);
200
+ res.setHeader('X-RateLimit-Limit', max);
201
+ res.setHeader('X-RateLimit-Remaining', Math.max(0, max - count));
202
+ if (count > max) return res.status(429).json({ error: 'Too many requests' });
203
+ next();
204
+ };
205
+ }
206
+
207
+ // ===== Env =====
208
+ export function loadEnv(spec) {
209
+ const env = {};
210
+ for (const [key, rules] of Object.entries(spec)) {
211
+ let val = process.env[key];
212
+ if (val === undefined && rules.default !== undefined) val = String(rules.default);
213
+ if (rules.required && val === undefined) {
214
+ console.error(`[NAIDE] Missing required env var: ${key}`);
215
+ process.exit(1);
216
+ }
217
+ if (val !== undefined) {
218
+ if (rules.type === 'number' || rules.type === 'integer') val = Number(val);
219
+ if (rules.type === 'boolean') val = val === 'true' || val === '1';
220
+ }
221
+ env[key] = val;
222
+ }
223
+ return env;
224
+ }
225
+
226
+ // ===== Cron / Scheduler =====
227
+ export function scheduleEvery(interval, fn) {
228
+ const ms = parseMs(interval);
229
+ const timer = setInterval(async () => {
230
+ try { await fn(); } catch (e) { console.error('[NAIDE Cron]', e.message); }
231
+ }, ms);
232
+ fn();
233
+ return timer;
234
+ }
235
+
236
+ // ===== Event Bus (for watch) =====
237
+ export function createEventBus() {
238
+ const listeners = new Map();
239
+ return {
240
+ on(event, fn) {
241
+ if (!listeners.has(event)) listeners.set(event, []);
242
+ listeners.get(event).push(fn);
243
+ },
244
+ emit(event, data) {
245
+ const fns = listeners.get(event) || [];
246
+ for (const fn of fns) {
247
+ try { fn({ event, data, timestamp: new Date().toISOString() }); } catch (e) { console.error('[NAIDE Event]', e.message); }
248
+ }
249
+ }
250
+ };
251
+ }
252
+
253
+ // ===== Helpers =====
254
+ function parseMs(str) {
255
+ if (typeof str === 'number') return str;
256
+ const m = str.match(/^(\d+)(ms|s|m|h|d)$/);
257
+ if (!m) return 60000;
258
+ const n = parseInt(m[1]);
259
+ switch (m[2]) {
260
+ case 'ms': return n;
261
+ case 's': return n * 1000;
262
+ case 'm': return n * 60000;
263
+ case 'h': return n * 3600000;
264
+ case 'd': return n * 86400000;
265
+ default: return 60000;
266
+ }
267
+ }
package/src/tokens.js CHANGED
@@ -57,6 +57,14 @@ export const T = {
57
57
  BREAK: 'BREAK',
58
58
  CONTINUE: 'CONTINUE',
59
59
  THROW: 'THROW',
60
+ SCHEMA: 'SCHEMA',
61
+ CRUD: 'CRUD',
62
+ AUTH: 'AUTH',
63
+ CORS: 'CORS',
64
+ LIMIT: 'LIMIT',
65
+ ENV: 'ENV',
66
+ EVERY: 'EVERY',
67
+ WATCH: 'WATCH',
60
68
 
61
69
  // Operators
62
70
  ASSIGN: 'ASSIGN',
@@ -142,6 +150,14 @@ export const KEYWORDS = {
142
150
  'break': T.BREAK,
143
151
  'continue': T.CONTINUE,
144
152
  'throw': T.THROW,
153
+ 'schema': T.SCHEMA,
154
+ 'crud': T.CRUD,
155
+ 'auth': T.AUTH,
156
+ 'cors': T.CORS,
157
+ 'limit': T.LIMIT,
158
+ 'env': T.ENV,
159
+ 'every': T.EVERY,
160
+ 'watch': T.WATCH,
145
161
  'true': T.BOOL,
146
162
  'false': T.BOOL,
147
163
  'null': T.NULL,