veko-framework 1.0.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 +233 -0
- package/bin/veko.js +79 -0
- package/index.js +94 -0
- package/package.json +71 -0
- package/src/cli/build.js +16 -0
- package/src/cli/check.ts +203 -0
- package/src/cli/dev.ts +189 -0
- package/src/cli/migrate.js +80 -0
- package/src/compiler/aot-validation.js +67 -0
- package/src/compiler/codegen.js +203 -0
- package/src/compiler/compile.d.ts +16 -0
- package/src/compiler/compile.js +39 -0
- package/src/compiler/index.js +5 -0
- package/src/compiler/lexer.js +114 -0
- package/src/compiler/parser.d.ts +7 -0
- package/src/compiler/parser.js +185 -0
- package/src/compiler/resolver.d.ts +10 -0
- package/src/compiler/resolver.js +88 -0
- package/src/compiler/source-utils.js +46 -0
- package/src/compiler/types.d.ts +50 -0
- package/src/compiler/types.js +15 -0
- package/src/config/config-types.js +35 -0
- package/src/config/defaults.d.ts +16 -0
- package/src/config/defaults.js +36 -0
- package/src/config/index.js +6 -0
- package/src/config/load-config.js +59 -0
- package/src/runtime/adapters/base.js +96 -0
- package/src/runtime/adapters/postgres.js +98 -0
- package/src/runtime/adapters/sqlite.js +87 -0
- package/src/runtime/auth.js +64 -0
- package/src/runtime/errors.js +63 -0
- package/src/runtime/index.js +36 -0
- package/src/runtime/queryBuilder.js +86 -0
- package/src/runtime/sugar.js +76 -0
- package/src/runtime/validator.js +71 -0
- package/src/runtime/wrapAction.js +24 -0
- package/tsconfig.json +17 -0
package/Readme.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/Veko-v1-2d3748?style=for-the-badge" alt="Veko v1" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="#benchmarks"><img src="https://img.shields.io/badge/Performance-22k%2B%20RPS-brightgreen?style=flat-square" alt="22k+ RPS" /></a>
|
|
7
|
+
<a href="#security"><img src="https://img.shields.io/badge/Security-Audited-blue?style=flat-square" alt="Security Audited" /></a>
|
|
8
|
+
<a href="#"><img src="https://img.shields.io/badge/Production-Ready-green?style=flat-square" alt="Production Ready" /></a>
|
|
9
|
+
<a href="#license"><img src="https://img.shields.io/badge/License-MIT-yellow?style=flat-square" alt="MIT" /></a>
|
|
10
|
+
<img src="https://img.shields.io/badge/Node.js-18%2B-339933?style=flat-square&logo=node.js" alt="Node 18+" />
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Veko v1
|
|
16
|
+
|
|
17
|
+
**An AOT-optimized DSL engine for Node.js** that compiles high-level schemas and actions into lean, JIT-friendly JavaScript. Build production-ready APIs with **Go-like performance** and the flexibility of Node.js.
|
|
18
|
+
|
|
19
|
+
- **Ahead-of-Time (AOT) Compilation** — Your `.vk` DSL is compiled to static Express route handlers. No runtime interpretation, no `eval`, no middleware overhead.
|
|
20
|
+
- **Security Audited** — Prototype pollution guards, strict type validation, parameterized SQL, and JWT algorithm enforcement are built in and verified.
|
|
21
|
+
- **High Concurrency** — SQLite WAL mode, prepared statements, and configurable `busy_timeout` for 13k+ RPS under load.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Table of Contents
|
|
26
|
+
|
|
27
|
+
- [Benchmarks](#benchmarks)
|
|
28
|
+
- [Key Features](#key-features)
|
|
29
|
+
- [Installation](#installation)
|
|
30
|
+
- [Quick Start](#quick-start)
|
|
31
|
+
- [DSL Overview](#dsl-overview)
|
|
32
|
+
- [Commands](#commands)
|
|
33
|
+
- [Security](#security)
|
|
34
|
+
- [License](#license)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Benchmarks
|
|
39
|
+
|
|
40
|
+
Veko v1 is built for throughput. All numbers from **autocannon** on typical hardware (single process, SQLite WAL).
|
|
41
|
+
|
|
42
|
+
| Scenario | Veko v1 AOT | Typical Express | Advantage |
|
|
43
|
+
|----------|-----------------|-----------------|-----------|
|
|
44
|
+
| **Plain ping** (no DB) | **~22,000+ RPS** | ~2,000–3,000 RPS | **~4×–5×** |
|
|
45
|
+
| **DB read** (SQLite, 100 conn) | **13,143 RPS** | ~2,500–4,000 RPS | **~4×** |
|
|
46
|
+
| **Avg latency** (DB read) | **~7 ms** | ~25–50 ms | Lower tail latency |
|
|
47
|
+
|
|
48
|
+
*Run your own: `npm run build` → `node generated/server.js` → `autocannon -c 100 -d 30 http://localhost:3000/db-test` (seed DB first with `node test/seed-db.js`).*
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Key Features
|
|
53
|
+
|
|
54
|
+
| Feature | Description |
|
|
55
|
+
|--------|-------------|
|
|
56
|
+
| **AOT Compilation** | DSL compiles directly to optimized route handlers. No middleware stack, no runtime schema loops. |
|
|
57
|
+
| **Built-in Security** | Passed security audit: prototype pollution protection, strict type validation, parameterized AOT queries, JWT algorithm enforcement and secret guards. |
|
|
58
|
+
| **Database** | Native **SQLite** and **PostgreSQL** support. WAL mode, `busy_timeout`, and retry-friendly design for high concurrency. |
|
|
59
|
+
| **Type-Safe DSL** | `data` blocks with `String`, `Number`, `Boolean`, `Enum`, optional fields, and min/max constraints. Generated TypeScript types. |
|
|
60
|
+
| **Pipeline Routing** | `auth`, `validate(Schema)`, and action steps in a single declarative route block. |
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
**From source (development):**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone <your-repo-url>
|
|
70
|
+
cd veko
|
|
71
|
+
npm install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Prerequisites:** Node.js 18+. Uses ESM and optional `tsx` for dev.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Quick Start
|
|
79
|
+
|
|
80
|
+
1. **Build** the project (compile `.vk` → `generated/server.js`):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx veko build
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
2. **Run** the generated server:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node generated/server.js
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
3. **(Optional)** Apply migrations and use dev mode:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx veko migrate
|
|
96
|
+
npx veko dev # watch + hot restart
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## DSL Overview
|
|
102
|
+
|
|
103
|
+
Your API lives in `.vk` files: **data** (schemas), **do** (actions), and **route** (HTTP pipelines).
|
|
104
|
+
|
|
105
|
+
### Data (schemas)
|
|
106
|
+
|
|
107
|
+
```vk
|
|
108
|
+
data User {
|
|
109
|
+
username: String(min:3, max:50),
|
|
110
|
+
email: String(max:255),
|
|
111
|
+
role: Enum(admin|editor|viewer),
|
|
112
|
+
age: Number(min:0, max:150)?
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
data Post {
|
|
116
|
+
title: String(min:1, max:200),
|
|
117
|
+
content: String(max:5000),
|
|
118
|
+
status: Enum(draft|published|archived),
|
|
119
|
+
authorId: Number
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Do blocks (business logic)
|
|
124
|
+
|
|
125
|
+
Use the **sugar** API for safe, allowlisted CRUD and **SELECT**-only raw queries:
|
|
126
|
+
|
|
127
|
+
```vk
|
|
128
|
+
do listUsers(data) {
|
|
129
|
+
return sugar.all('User');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
do createUser(data) {
|
|
133
|
+
return sugar.save('User', data);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
do getUser(data) {
|
|
137
|
+
const id = parseInt(data.id ?? data.params?.id);
|
|
138
|
+
const user = sugar.find('User', id);
|
|
139
|
+
if (!user) throw Object.assign(new Error('User not found'), { status: 404 });
|
|
140
|
+
return user;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
do listPosts(data) {
|
|
144
|
+
return sugar.query(
|
|
145
|
+
`SELECT p.*, u.username as authorName
|
|
146
|
+
FROM "Post" p
|
|
147
|
+
LEFT JOIN "User" u ON p.authorId = u.id
|
|
148
|
+
ORDER BY p.id DESC`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Routes (pipelines)
|
|
154
|
+
|
|
155
|
+
Chain **auth**, **validate(Schema)**, and action names:
|
|
156
|
+
|
|
157
|
+
```vk
|
|
158
|
+
route {
|
|
159
|
+
GET "/ping" => ping
|
|
160
|
+
GET "/db-test" => dbTest
|
|
161
|
+
|
|
162
|
+
POST "/login" => login
|
|
163
|
+
POST "/register" => validate(User), createUser
|
|
164
|
+
|
|
165
|
+
GET "/users" => listUsers
|
|
166
|
+
GET "/users/:id" => getUser
|
|
167
|
+
POST "/users" => auth, validate(User), createUser
|
|
168
|
+
|
|
169
|
+
GET "/posts" => listPosts
|
|
170
|
+
POST "/posts" => auth, validate(Post), createPost
|
|
171
|
+
DELETE "/posts/:id" => auth, deletePost
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Imports
|
|
176
|
+
|
|
177
|
+
Compose multiple `.vk` files from your entry (e.g. `api.vk`):
|
|
178
|
+
|
|
179
|
+
```vk
|
|
180
|
+
import "./models.vk"
|
|
181
|
+
import "./handlers.vk"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Commands
|
|
187
|
+
|
|
188
|
+
| Command | Description |
|
|
189
|
+
|--------|-------------|
|
|
190
|
+
| `npx veko build` | Compile `api.vk` → `generated/server.js` + `generated/types.d.ts`. |
|
|
191
|
+
| `npx veko dev` | Watch `.vk` files, rebuild and restart the server (hot reload). |
|
|
192
|
+
| `npx veko check` | Static analysis: schema integrity, security scan, circular deps. |
|
|
193
|
+
| `npx veko migrate` | Apply SQL migrations. Use `-t` for test DB. |
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Project structure
|
|
198
|
+
|
|
199
|
+
The framework is organized for clarity and scalability:
|
|
200
|
+
|
|
201
|
+
| Path | Purpose |
|
|
202
|
+
|------|---------|
|
|
203
|
+
| `src/core/` | Shared config and constants (e.g. `defaults.js`). |
|
|
204
|
+
| `src/compiler/` | Compiler entry (`compile.js`) and subpackages: |
|
|
205
|
+
| `src/compiler/parse/` | Lexer, parser, and source utilities. |
|
|
206
|
+
| `src/compiler/codegen/` | AOT code generation and validation codegen. |
|
|
207
|
+
| `src/compiler/resolve/` | Module resolution and AST merging for `.vk` imports. |
|
|
208
|
+
| `src/runtime/` | Runtime used by generated server (adapters, auth, errors, etc.). |
|
|
209
|
+
| `src/cli/` | CLI commands: build, dev, check, migrate. |
|
|
210
|
+
|
|
211
|
+
Generated output goes to `generated/server.js` and `generated/types.d.ts`.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Security
|
|
216
|
+
|
|
217
|
+
Veko v1 is designed and audited for production use:
|
|
218
|
+
|
|
219
|
+
- **Prototype pollution protection** — Request context uses validated/null-prototype data; schema iteration does not rely on user-controlled keys.
|
|
220
|
+
- **Strict type validation** — AOT-generated validators with number coercion guards and enum allowlists.
|
|
221
|
+
- **SQL injection prevention** — All table CRUD uses prepared statements; `sugar.query` is **SELECT-only** and parameterized; AOT allowlist protects `findAll` column names.
|
|
222
|
+
- **JWT security** — Algorithm restricted to `HS256`; authorization header normalized (array/string); no algorithm confusion.
|
|
223
|
+
- **Database** — SQLite WAL + `busy_timeout` for predictable behavior under contention; no raw string interpolation in generated SQL.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
**MIT License**
|
|
230
|
+
|
|
231
|
+
Copyright (c) 2024–2026 Wai Wai Naing.
|
|
232
|
+
|
|
233
|
+
See the [LICENSE](LICENSE) file for full text.
|
package/bin/veko.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Veko v1 – CLI (build | dev | check | migrate)
|
|
4
|
+
* Run: npx veko <command> [options]
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { readFileSync } from 'fs';
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
15
|
+
|
|
16
|
+
const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf-8'));
|
|
17
|
+
const VERSION = pkg.version || '1.0.0';
|
|
18
|
+
|
|
19
|
+
const CLI_SCRIPTS = {
|
|
20
|
+
build: { script: 'build.js', runner: 'node' },
|
|
21
|
+
migrate: { script: 'migrate.js', runner: 'node' },
|
|
22
|
+
check: { script: 'check.ts', runner: 'tsx' },
|
|
23
|
+
dev: { script: 'dev.ts', runner: 'tsx' },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function runCommand(name, extraArgs = []) {
|
|
27
|
+
const config = CLI_SCRIPTS[name];
|
|
28
|
+
if (!config) {
|
|
29
|
+
console.error(`Unknown command: ${name}`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const scriptPath = path.join(ROOT, 'src', 'cli', config.script);
|
|
33
|
+
const runner = config.runner === 'tsx' ? 'npx' : 'node';
|
|
34
|
+
const args = config.runner === 'tsx' ? ['tsx', scriptPath, ...extraArgs] : [scriptPath, ...extraArgs];
|
|
35
|
+
const result = spawnSync(runner, args, {
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
});
|
|
39
|
+
process.exit(result.status != null ? result.status : result.signal ? 1 : 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const program = new Command();
|
|
43
|
+
|
|
44
|
+
program
|
|
45
|
+
.name('veko')
|
|
46
|
+
.description('Veko v1 – compile .vk to Express + SQLite')
|
|
47
|
+
.version(VERSION);
|
|
48
|
+
|
|
49
|
+
program
|
|
50
|
+
.command('build')
|
|
51
|
+
.description('Compile api.vk → generated/server.js + types.d.ts')
|
|
52
|
+
.action(() => runCommand('build'));
|
|
53
|
+
|
|
54
|
+
program
|
|
55
|
+
.command('dev')
|
|
56
|
+
.description('Watch .vk files, rebuild and run server (hot restart)')
|
|
57
|
+
.action(() => runCommand('dev'));
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command('check')
|
|
61
|
+
.description('Static analysis: schema, security, routes, circular deps')
|
|
62
|
+
.action(() => runCommand('check'));
|
|
63
|
+
|
|
64
|
+
program
|
|
65
|
+
.command('migrate')
|
|
66
|
+
.description('Apply versioned migrations (SQLite)')
|
|
67
|
+
.option('-t, --test', 'Use test database (NODE_ENV=test)')
|
|
68
|
+
.action((opts) => {
|
|
69
|
+
if (opts.test) {
|
|
70
|
+
process.env.NODE_ENV = 'test';
|
|
71
|
+
}
|
|
72
|
+
runCommand('migrate');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
program.parse();
|
|
76
|
+
|
|
77
|
+
if (!process.argv.slice(2).length) {
|
|
78
|
+
program.outputHelp();
|
|
79
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Veko v1 – Framework entry & DX helpers
|
|
3
|
+
* Use defineConfig in veko.config.js for typed config with defaults.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { DEFAULTS } from './src/config/defaults.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Default configuration values (used when a key is omitted)
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
entry: './api.vk',
|
|
13
|
+
output: {
|
|
14
|
+
server: './generated/server.js',
|
|
15
|
+
types: './generated/types.d.ts',
|
|
16
|
+
},
|
|
17
|
+
adapter: 'sqlite',
|
|
18
|
+
database: {
|
|
19
|
+
sqlite: { path: DEFAULTS.DB_FILE },
|
|
20
|
+
postgres: { connectionString: undefined, ssl: false },
|
|
21
|
+
},
|
|
22
|
+
server: { port: DEFAULTS.PORT },
|
|
23
|
+
migrations: {
|
|
24
|
+
table: '_veko_migrations',
|
|
25
|
+
dir: './migrations',
|
|
26
|
+
},
|
|
27
|
+
plugins: [],
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Deep-merge defaults with user config (shallow merge per top-level key).
|
|
32
|
+
* Environment-aware: database.sqlite.path uses DB_FILE or NODE_ENV=test → veko.test.db.
|
|
33
|
+
*
|
|
34
|
+
* @param {Partial<import('./src/config/config-types.js').VekoConfig>} config - User config (partial)
|
|
35
|
+
* @returns {import('./src/config/config-types.js').VekoConfig} Full config with defaults
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // veko.config.js
|
|
39
|
+
* import { defineConfig } from 'veko';
|
|
40
|
+
* export default defineConfig({
|
|
41
|
+
* entry: './api.vk',
|
|
42
|
+
* adapter: 'postgres',
|
|
43
|
+
* database: { postgres: { connectionString: process.env.DATABASE_URL } },
|
|
44
|
+
* });
|
|
45
|
+
*/
|
|
46
|
+
export function defineConfig(config = {}) {
|
|
47
|
+
const dbPath =
|
|
48
|
+
process.env.DB_FILE ||
|
|
49
|
+
(process.env.NODE_ENV === 'test' ? DEFAULTS.DB_FILE_TEST : DEFAULTS.DB_FILE);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...DEFAULT_CONFIG,
|
|
53
|
+
...config,
|
|
54
|
+
entry: config.entry ?? DEFAULT_CONFIG.entry,
|
|
55
|
+
output: {
|
|
56
|
+
...DEFAULT_CONFIG.output,
|
|
57
|
+
...config.output,
|
|
58
|
+
},
|
|
59
|
+
adapter: config.adapter ?? DEFAULT_CONFIG.adapter,
|
|
60
|
+
database: {
|
|
61
|
+
sqlite: {
|
|
62
|
+
...DEFAULT_CONFIG.database.sqlite,
|
|
63
|
+
...config.database?.sqlite,
|
|
64
|
+
path: config.database?.sqlite?.path ?? dbPath,
|
|
65
|
+
},
|
|
66
|
+
postgres: {
|
|
67
|
+
...DEFAULT_CONFIG.database.postgres,
|
|
68
|
+
...config.database?.postgres,
|
|
69
|
+
connectionString:
|
|
70
|
+
config.database?.postgres?.connectionString ??
|
|
71
|
+
process.env.DATABASE_URL,
|
|
72
|
+
ssl:
|
|
73
|
+
config.database?.postgres?.ssl ??
|
|
74
|
+
(process.env.NODE_ENV === 'production'
|
|
75
|
+
? { rejectUnauthorized: true }
|
|
76
|
+
: false),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
server: {
|
|
80
|
+
...DEFAULT_CONFIG.server,
|
|
81
|
+
...config.server,
|
|
82
|
+
port:
|
|
83
|
+
config.server?.port ??
|
|
84
|
+
parseInt(process.env.PORT || String(DEFAULTS.PORT), 10),
|
|
85
|
+
},
|
|
86
|
+
migrations: {
|
|
87
|
+
...DEFAULT_CONFIG.migrations,
|
|
88
|
+
...config.migrations,
|
|
89
|
+
},
|
|
90
|
+
plugins: config.plugins ?? DEFAULT_CONFIG.plugins,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { defineConfig as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "veko-framework",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./index.js",
|
|
8
|
+
"./config": "./src/config/index.js",
|
|
9
|
+
"./config/defaults": "./src/config/defaults.js",
|
|
10
|
+
"./config/load-config": "./src/config/load-config.js",
|
|
11
|
+
"./runtime": "./src/runtime/index.js",
|
|
12
|
+
"./runtime/auth": "./src/runtime/auth.js",
|
|
13
|
+
"./runtime/errors": "./src/runtime/errors.js",
|
|
14
|
+
"./runtime/wrapAction": "./src/runtime/wrapAction.js",
|
|
15
|
+
"./runtime/queryBuilder": "./src/runtime/queryBuilder.js",
|
|
16
|
+
"./runtime/sugar": "./src/runtime/sugar.js",
|
|
17
|
+
"./runtime/validator": "./src/runtime/validator.js",
|
|
18
|
+
"./runtime/adapters/sqlite": "./src/runtime/adapters/sqlite.js",
|
|
19
|
+
"./runtime/adapters/postgres": "./src/runtime/adapters/postgres.js",
|
|
20
|
+
"./runtime/adapters/base": "./src/runtime/adapters/base.js",
|
|
21
|
+
"./compiler": "./src/compiler/index.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"bcrypt": "^6.0.0",
|
|
25
|
+
"better-sqlite3": "^11.0.0",
|
|
26
|
+
"chalk": "^5.3.0",
|
|
27
|
+
"chokidar": "^4.0.1",
|
|
28
|
+
"commander": "^12.0.0",
|
|
29
|
+
"cors": "^2.8.5",
|
|
30
|
+
"express": "^4.21.0",
|
|
31
|
+
"express-rate-limit": "^7.1.5",
|
|
32
|
+
"express-validator": "^7.0.1",
|
|
33
|
+
"helmet": "^7.1.0",
|
|
34
|
+
"jsonwebtoken": "^9.0.2",
|
|
35
|
+
"pino": "^8.17.2",
|
|
36
|
+
"prom-client": "^15.1.0",
|
|
37
|
+
"swagger-ui-express": "^5.0.0",
|
|
38
|
+
"vm2": "^3.9.19"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.10.0",
|
|
42
|
+
"autocannon": "^8.0.0",
|
|
43
|
+
"tsx": "^4.7.0",
|
|
44
|
+
"typescript": "^5.3.3"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"start": "node src/legacy/compiler-v3.js",
|
|
48
|
+
"build": "node src/cli/build.js",
|
|
49
|
+
"dev": "tsx src/cli/dev.ts",
|
|
50
|
+
"migrate": "node src/cli/migrate.js",
|
|
51
|
+
"migrate:test": "NODE_ENV=test node src/cli/migrate.js",
|
|
52
|
+
"check": "tsx src/cli/check.ts",
|
|
53
|
+
"start:generated": "npm run build && node generated/server.js",
|
|
54
|
+
"start:test": "npm run build && NODE_ENV=test DB_FILE=veko.test.db PORT=3001 node generated/server.js",
|
|
55
|
+
"test": "npm run check && npm run build",
|
|
56
|
+
"test:auth": "node scripts/test-auth.mjs",
|
|
57
|
+
"bench": "node test/bench-report.js",
|
|
58
|
+
"test:integration": "node test/integration.test.mjs"
|
|
59
|
+
},
|
|
60
|
+
"bin": {
|
|
61
|
+
"veko": "./bin/veko.js"
|
|
62
|
+
},
|
|
63
|
+
"files": [
|
|
64
|
+
"index.js",
|
|
65
|
+
"bin",
|
|
66
|
+
"src",
|
|
67
|
+
"tsconfig.json",
|
|
68
|
+
"README.md",
|
|
69
|
+
"LICENSE"
|
|
70
|
+
]
|
|
71
|
+
}
|
package/src/cli/build.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Veko v1 – Build (compile api.vk → generated/server.js + types.d.ts)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { compile } from '../compiler/compile.js';
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const { output } = await compile();
|
|
10
|
+
console.log('Veko v1 build OK:', output.server, output.types);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
main().catch((err) => {
|
|
14
|
+
console.error(err);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
package/src/cli/check.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Veko v1 – Static analysis (check) CLI
|
|
4
|
+
* Schema integrity, security scan, route validation, circular dependency.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const ROOT = process.cwd();
|
|
13
|
+
|
|
14
|
+
const VALID_SCHEMA_TYPES = new Set([
|
|
15
|
+
'String',
|
|
16
|
+
'Number',
|
|
17
|
+
'Boolean',
|
|
18
|
+
'Enum',
|
|
19
|
+
'Date',
|
|
20
|
+
'Password',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const FORBIDDEN_PATTERNS = [
|
|
24
|
+
{ pattern: /\brequire\s*\(/g, keyword: 'require' },
|
|
25
|
+
{ pattern: /\bimport\s*\(/g, keyword: 'import' },
|
|
26
|
+
{ pattern: /\bprocess\b/g, keyword: 'process' },
|
|
27
|
+
{ pattern: /\beval\s*\(/g, keyword: 'eval' },
|
|
28
|
+
{ pattern: /\bchild_process\b/g, keyword: 'child_process' },
|
|
29
|
+
{ pattern: /\bFunction\s*\(/g, keyword: 'Function' },
|
|
30
|
+
{ pattern: /\bglobal\s*[=.]/g, keyword: 'global' },
|
|
31
|
+
{ pattern: /\b__dirname\b/g, keyword: '__dirname' },
|
|
32
|
+
{ pattern: /\b__filename\b/g, keyword: '__filename' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
interface CheckIssue {
|
|
36
|
+
line: number;
|
|
37
|
+
rule: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function lineOffsetInBody(body: string, index: number): number {
|
|
42
|
+
return body.slice(0, index).split('\n').length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function runCheck(): Promise<CheckIssue[]> {
|
|
46
|
+
const { loadConfig } = await import('../compiler/compile.js');
|
|
47
|
+
const { parse } = await import('../compiler/parser.js');
|
|
48
|
+
const config = await loadConfig(ROOT);
|
|
49
|
+
const entry = config.entry as string;
|
|
50
|
+
const fs = await import('fs');
|
|
51
|
+
if (!fs.existsSync(entry)) {
|
|
52
|
+
throw new Error(`Entry file not found: ${entry}`);
|
|
53
|
+
}
|
|
54
|
+
const { resolveModules } = await import('../compiler/resolver.js');
|
|
55
|
+
let ast: {
|
|
56
|
+
dataBlocks: Array<{ name: string; fields: Array<{ name: string; type: string; optional?: boolean; args?: Record<string, unknown> }> }>;
|
|
57
|
+
doBlocks: Array<{ name: string; body: string; line: number }>;
|
|
58
|
+
routeLines: Array<{
|
|
59
|
+
method: string;
|
|
60
|
+
path: string;
|
|
61
|
+
pipeline: Array<{ kind: string; name?: string; schema?: string }>;
|
|
62
|
+
}>;
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
ast = resolveModules(entry, config.root) as typeof ast;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
throw new Error(`Resolve failed: ${msg}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const issues: CheckIssue[] = [];
|
|
72
|
+
const dataBlockNames = new Set(ast.dataBlocks.map((b) => b.name));
|
|
73
|
+
const doBlockNames = new Set(ast.doBlocks.map((d) => d.name));
|
|
74
|
+
|
|
75
|
+
// ─── 1. Schema Integrity ─────────────────────────────────────────────
|
|
76
|
+
for (const block of ast.dataBlocks) {
|
|
77
|
+
for (const field of block.fields) {
|
|
78
|
+
const t = field.type;
|
|
79
|
+
const isValidBuiltin = VALID_SCHEMA_TYPES.has(t);
|
|
80
|
+
const isOtherBlock = dataBlockNames.has(t);
|
|
81
|
+
if (!isValidBuiltin && !isOtherBlock) {
|
|
82
|
+
issues.push({
|
|
83
|
+
line: 0,
|
|
84
|
+
rule: 'Schema Integrity',
|
|
85
|
+
message: `data ${block.name}: field "${field.name}" has invalid type "${t}". Valid types: ${[...VALID_SCHEMA_TYPES].join(', ')}, or another data block name.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ─── 2. Security Scan (do blocks) ────────────────────────────────────
|
|
92
|
+
for (const block of ast.doBlocks) {
|
|
93
|
+
const body = block.body;
|
|
94
|
+
const bodyStartLine = block.line + 1;
|
|
95
|
+
for (const { pattern, keyword } of FORBIDDEN_PATTERNS) {
|
|
96
|
+
pattern.lastIndex = 0;
|
|
97
|
+
let m: RegExpExecArray | null;
|
|
98
|
+
while ((m = pattern.exec(body)) !== null) {
|
|
99
|
+
const line = bodyStartLine + lineOffsetInBody(body, m.index) - 1;
|
|
100
|
+
issues.push({
|
|
101
|
+
line,
|
|
102
|
+
rule: 'Security Scan',
|
|
103
|
+
message: `do ${block.name}: forbidden keyword "${keyword}" is not allowed in action bodies.`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── 3. Route Validation ─────────────────────────────────────────────
|
|
110
|
+
for (const route of ast.routeLines) {
|
|
111
|
+
for (const step of route.pipeline) {
|
|
112
|
+
if (step.kind === 'action' && step.name) {
|
|
113
|
+
if (!doBlockNames.has(step.name)) {
|
|
114
|
+
issues.push({
|
|
115
|
+
line: 0,
|
|
116
|
+
rule: 'Route Validation',
|
|
117
|
+
message: `Route ${route.method} ${route.path}: action "${step.name}" is not defined in any do block.`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (step.kind === 'validate' && step.schema) {
|
|
122
|
+
if (!dataBlockNames.has(step.schema)) {
|
|
123
|
+
issues.push({
|
|
124
|
+
line: 0,
|
|
125
|
+
rule: 'Route Validation',
|
|
126
|
+
message: `Route ${route.method} ${route.path}: validate(${step.schema}) references unknown schema. No data block named "${step.schema}".`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── 4. Circular Dependency (data block references) ───────────────────
|
|
134
|
+
const refs = new Map<string, string[]>();
|
|
135
|
+
for (const block of ast.dataBlocks) {
|
|
136
|
+
const used: string[] = [];
|
|
137
|
+
for (const field of block.fields) {
|
|
138
|
+
if (dataBlockNames.has(field.type) && field.type !== block.name) {
|
|
139
|
+
used.push(field.type);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
refs.set(block.name, used);
|
|
143
|
+
}
|
|
144
|
+
const visited = new Set<string>();
|
|
145
|
+
const stack = new Set<string>();
|
|
146
|
+
const pathStack: string[] = [];
|
|
147
|
+
const reportedCycles = new Set<string>();
|
|
148
|
+
|
|
149
|
+
function visit(name: string): boolean {
|
|
150
|
+
if (stack.has(name)) {
|
|
151
|
+
const idx = pathStack.indexOf(name);
|
|
152
|
+
const cycle = [...pathStack.slice(idx), name];
|
|
153
|
+
const cycleKey = [...new Set(cycle)].sort().join('→');
|
|
154
|
+
if (!reportedCycles.has(cycleKey)) {
|
|
155
|
+
reportedCycles.add(cycleKey);
|
|
156
|
+
issues.push({
|
|
157
|
+
line: 0,
|
|
158
|
+
rule: 'Circular Dependency',
|
|
159
|
+
message: `Data blocks have a circular reference: ${cycle.join(' → ')}. This can break table creation order or cause infinite recursion.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (visited.has(name)) return false;
|
|
165
|
+
visited.add(name);
|
|
166
|
+
stack.add(name);
|
|
167
|
+
pathStack.push(name);
|
|
168
|
+
for (const next of refs.get(name) ?? []) {
|
|
169
|
+
visit(next);
|
|
170
|
+
}
|
|
171
|
+
pathStack.pop();
|
|
172
|
+
stack.delete(name);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
for (const block of ast.dataBlocks) {
|
|
176
|
+
visit(block.name);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return issues;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function main(): void {
|
|
183
|
+
const prefix = chalk.gray('[veko check]');
|
|
184
|
+
runCheck()
|
|
185
|
+
.then((issues) => {
|
|
186
|
+
if (issues.length === 0) {
|
|
187
|
+
console.log(prefix, chalk.green('✅ All checks passed. Your DSL is production-ready.'));
|
|
188
|
+
process.exit(0);
|
|
189
|
+
}
|
|
190
|
+
console.error(prefix, chalk.red(`Found ${issues.length} issue(s):\n`));
|
|
191
|
+
for (const issue of issues) {
|
|
192
|
+
const linePart = issue.line > 0 ? chalk.cyan(` Line ${issue.line}: `) : ' ';
|
|
193
|
+
console.error(linePart + chalk.yellow(`[${issue.rule}] `) + issue.message);
|
|
194
|
+
}
|
|
195
|
+
process.exit(1);
|
|
196
|
+
})
|
|
197
|
+
.catch((err) => {
|
|
198
|
+
console.error(prefix, chalk.red(err instanceof Error ? err.message : String(err)));
|
|
199
|
+
process.exit(1);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
main();
|