naidejs 1.1.0 → 1.2.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 +183 -4
- package/SPEC-X.nx +80 -7
- package/SPEC.naide +117 -9
- package/bin/naide.js +55 -1
- package/examples/fullapp.naide +41 -4
- package/examples/fullapp.nx +41 -4
- package/package.json +9 -5
- package/src/generator.js +248 -5
- package/src/parser.js +143 -90
- package/src/preprocess.js +8 -0
- package/src/runtime.js +162 -4
- package/src/tokens.js +8 -0
package/src/runtime.js
CHANGED
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
import { createHmac, randomUUID, timingSafeEqual } from 'crypto';
|
|
1
|
+
import { createHmac, randomUUID, timingSafeEqual, scryptSync, randomBytes } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
|
|
5
|
+
// ===== Password Hashing (zero-dep, Node crypto) =====
|
|
6
|
+
export function hash(password) {
|
|
7
|
+
const salt = randomBytes(16).toString('hex');
|
|
8
|
+
const derived = scryptSync(password, salt, 64).toString('hex');
|
|
9
|
+
return `${salt}:${derived}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function verify(password, stored) {
|
|
13
|
+
const [salt, h] = stored.split(':');
|
|
14
|
+
const hashBuf = Buffer.from(h, 'hex');
|
|
15
|
+
const testBuf = scryptSync(password, salt, 64);
|
|
16
|
+
return timingSafeEqual(hashBuf, testBuf);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ===== UUID =====
|
|
20
|
+
export function uuid() {
|
|
21
|
+
return randomUUID();
|
|
22
|
+
}
|
|
2
23
|
|
|
3
24
|
// ===== Schema + Validation =====
|
|
4
25
|
export function createSchema(name, fieldDefs) {
|
|
@@ -93,10 +114,96 @@ export function createStore(schema) {
|
|
|
93
114
|
};
|
|
94
115
|
}
|
|
95
116
|
|
|
96
|
-
// =====
|
|
117
|
+
// ===== File-Based Persistent Store =====
|
|
118
|
+
export function createFileStore(schema, filePath) {
|
|
119
|
+
let _idField = null;
|
|
120
|
+
for (const [f, r] of Object.entries(schema.fields)) {
|
|
121
|
+
if (r.auto && r.type === 'id') { _idField = f; break; }
|
|
122
|
+
}
|
|
123
|
+
const idField = _idField || 'id';
|
|
124
|
+
|
|
125
|
+
const dir = dirname(filePath);
|
|
126
|
+
if (dir && dir !== '.' && !existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
127
|
+
|
|
128
|
+
const items = new Map();
|
|
129
|
+
try {
|
|
130
|
+
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
131
|
+
for (const item of data) items.set(String(item[idField] || item.id), item);
|
|
132
|
+
} catch {}
|
|
133
|
+
|
|
134
|
+
function save() {
|
|
135
|
+
writeFileSync(filePath, JSON.stringify([...items.values()], null, 2));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
getAll() { return [...items.values()]; },
|
|
140
|
+
getById(id) { return items.get(String(id)) || null; },
|
|
141
|
+
count() { return items.size; },
|
|
142
|
+
create(data) {
|
|
143
|
+
const { valid, errors, data: validated } = schema.validate(data);
|
|
144
|
+
if (!valid) return { error: errors };
|
|
145
|
+
const id = validated[idField] || randomUUID();
|
|
146
|
+
validated[idField] = id;
|
|
147
|
+
items.set(String(id), validated);
|
|
148
|
+
save();
|
|
149
|
+
return validated;
|
|
150
|
+
},
|
|
151
|
+
update(id, data) {
|
|
152
|
+
const existing = items.get(String(id));
|
|
153
|
+
if (!existing) return null;
|
|
154
|
+
const merged = { ...existing, ...data, [idField]: existing[idField] };
|
|
155
|
+
items.set(String(id), merged);
|
|
156
|
+
save();
|
|
157
|
+
return merged;
|
|
158
|
+
},
|
|
159
|
+
delete(id) {
|
|
160
|
+
const result = items.delete(String(id));
|
|
161
|
+
if (result) save();
|
|
162
|
+
return result;
|
|
163
|
+
},
|
|
164
|
+
where(conditions) {
|
|
165
|
+
return [...items.values()].filter(item => {
|
|
166
|
+
for (const [k, v] of Object.entries(conditions)) {
|
|
167
|
+
if (item[k] !== v) return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
clear() { items.clear(); save(); }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ===== CRUD Route Registration (with pagination, search, sort) =====
|
|
97
177
|
export function registerCrud(app, basePath, schema, store, eventBus) {
|
|
98
|
-
app.get(basePath, (
|
|
99
|
-
|
|
178
|
+
app.get(basePath, (req, res) => {
|
|
179
|
+
let items = store.getAll();
|
|
180
|
+
|
|
181
|
+
const q = req.query.q;
|
|
182
|
+
if (q) {
|
|
183
|
+
const lower = q.toLowerCase();
|
|
184
|
+
items = items.filter(item =>
|
|
185
|
+
Object.values(item).some(v =>
|
|
186
|
+
typeof v === 'string' && v.toLowerCase().includes(lower)
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (req.query.sort) {
|
|
192
|
+
const field = req.query.sort;
|
|
193
|
+
const order = req.query.order === 'desc' ? -1 : 1;
|
|
194
|
+
items.sort((a, b) => {
|
|
195
|
+
if (a[field] < b[field]) return -order;
|
|
196
|
+
if (a[field] > b[field]) return order;
|
|
197
|
+
return 0;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const total = items.length;
|
|
202
|
+
const page = parseInt(req.query.page) || 1;
|
|
203
|
+
const limit = Math.min(parseInt(req.query.limit) || 20, 100);
|
|
204
|
+
items = items.slice((page - 1) * limit, page * limit);
|
|
205
|
+
|
|
206
|
+
res.json({ data: items, total, page, limit, pages: Math.ceil(total / limit) });
|
|
100
207
|
});
|
|
101
208
|
|
|
102
209
|
app.get(`${basePath}/:id`, (req, res) => {
|
|
@@ -250,6 +357,57 @@ export function createEventBus() {
|
|
|
250
357
|
};
|
|
251
358
|
}
|
|
252
359
|
|
|
360
|
+
// ===== HTTP Client (zero-dep, Node 18+ fetch) =====
|
|
361
|
+
async function jsonOrText(res) {
|
|
362
|
+
const ct = res.headers.get('content-type') || '';
|
|
363
|
+
return ct.includes('json') ? res.json() : res.text();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export const api = {
|
|
367
|
+
async get(url, opts = {}) {
|
|
368
|
+
const res = await fetch(url, { ...opts, method: 'GET' });
|
|
369
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
370
|
+
return jsonOrText(res);
|
|
371
|
+
},
|
|
372
|
+
async post(url, body, opts = {}) {
|
|
373
|
+
const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', ...opts.headers }, body: JSON.stringify(body), ...opts });
|
|
374
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
375
|
+
return jsonOrText(res);
|
|
376
|
+
},
|
|
377
|
+
async put(url, body, opts = {}) {
|
|
378
|
+
const res = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...opts.headers }, body: JSON.stringify(body), ...opts });
|
|
379
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
380
|
+
return jsonOrText(res);
|
|
381
|
+
},
|
|
382
|
+
async del(url, opts = {}) {
|
|
383
|
+
const res = await fetch(url, { method: 'DELETE', ...opts });
|
|
384
|
+
if (!res.ok) throw Object.assign(new Error(`HTTP ${res.status}`), { status: res.status, response: res });
|
|
385
|
+
return jsonOrText(res);
|
|
386
|
+
},
|
|
387
|
+
async raw(url, opts = {}) {
|
|
388
|
+
return fetch(url, opts);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// ===== Cookie Parser =====
|
|
393
|
+
export function cookieParser() {
|
|
394
|
+
return (req, res, next) => {
|
|
395
|
+
req.cookies = {};
|
|
396
|
+
const header = req.headers.cookie;
|
|
397
|
+
if (header) {
|
|
398
|
+
for (const pair of header.split(';')) {
|
|
399
|
+
const idx = pair.indexOf('=');
|
|
400
|
+
if (idx > 0) {
|
|
401
|
+
const k = pair.slice(0, idx).trim();
|
|
402
|
+
const v = pair.slice(idx + 1).trim();
|
|
403
|
+
try { req.cookies[k] = decodeURIComponent(v); } catch { req.cookies[k] = v; }
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
next();
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
253
411
|
// ===== Helpers =====
|
|
254
412
|
function parseMs(str) {
|
|
255
413
|
if (typeof str === 'number') return str;
|
package/src/tokens.js
CHANGED
|
@@ -65,6 +65,10 @@ export const T = {
|
|
|
65
65
|
ENV: 'ENV',
|
|
66
66
|
EVERY: 'EVERY',
|
|
67
67
|
WATCH: 'WATCH',
|
|
68
|
+
STATIC: 'STATIC',
|
|
69
|
+
WS: 'WS',
|
|
70
|
+
GROUP: 'GROUP',
|
|
71
|
+
COOKIE: 'COOKIE',
|
|
68
72
|
|
|
69
73
|
// Operators
|
|
70
74
|
ASSIGN: 'ASSIGN',
|
|
@@ -158,6 +162,10 @@ export const KEYWORDS = {
|
|
|
158
162
|
'env': T.ENV,
|
|
159
163
|
'every': T.EVERY,
|
|
160
164
|
'watch': T.WATCH,
|
|
165
|
+
'static': T.STATIC,
|
|
166
|
+
'ws': T.WS,
|
|
167
|
+
'group': T.GROUP,
|
|
168
|
+
'cookie': T.COOKIE,
|
|
161
169
|
'true': T.BOOL,
|
|
162
170
|
'false': T.BOOL,
|
|
163
171
|
'null': T.NULL,
|