tina4-nodejs 3.10.91 → 3.10.93
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/package.json +1 -1
- package/packages/core/src/ai.ts +1 -1
- package/packages/core/src/devAdmin.ts +85 -7
- package/packages/core/src/devMailbox.ts +21 -21
- package/packages/core/src/graphql.ts +36 -0
- package/packages/core/src/i18n.ts +12 -0
- package/packages/core/src/index.ts +6 -6
- package/packages/core/src/messenger.ts +52 -4
- package/packages/core/src/middleware.ts +37 -0
- package/packages/core/src/rateLimiter.ts +88 -1
- package/packages/core/src/request.ts +24 -1
- package/packages/core/src/response.ts +54 -10
- package/packages/core/src/scss.ts +44 -2
- package/packages/core/src/server.ts +86 -9
- package/packages/core/src/service.ts +7 -0
- package/packages/core/src/testClient.ts +2 -2
- package/packages/core/src/testing.ts +6 -6
- package/packages/core/src/types.ts +8 -1
- package/packages/core/src/watcher.ts +66 -0
- package/packages/core/src/websocket.ts +2 -2
- package/packages/core/src/websocketConnection.ts +4 -0
- package/packages/core/src/wsdl.ts +12 -12
- package/packages/frond/src/engine.ts +103 -11
- package/packages/orm/src/autoCrud.ts +117 -74
- package/packages/orm/src/baseModel.ts +1 -1
- package/packages/orm/src/databaseResult.ts +5 -0
- package/packages/orm/src/index.ts +1 -1
- package/packages/orm/src/migration.ts +5 -2
- package/packages/orm/src/queryBuilder.ts +2 -2
- package/packages/swagger/src/generator.ts +2 -2
- package/packages/swagger/src/index.ts +1 -1
|
@@ -357,10 +357,26 @@ function resolveVar(expr: string, context: Record<string, unknown>): unknown {
|
|
|
357
357
|
return null;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
// Slice syntax: value[1:5], value[:10], value[start:end]
|
|
361
|
+
const isQuotedPart = (part.startsWith('"') && part.endsWith('"')) ||
|
|
362
|
+
(part.startsWith("'") && part.endsWith("'"));
|
|
363
|
+
if (isBracket && part.includes(":") && !isQuotedPart) {
|
|
364
|
+
const sliceParts = part.split(":", 2);
|
|
365
|
+
const sStart = sliceParts[0].trim() ? parseInt(String(evalExpr(sliceParts[0].trim(), context)), 10) : undefined;
|
|
366
|
+
const sEnd = sliceParts[1].trim() ? parseInt(String(evalExpr(sliceParts[1].trim(), context)), 10) : undefined;
|
|
367
|
+
if (Array.isArray(value)) {
|
|
368
|
+
value = (value as unknown[]).slice(sStart ?? 0, sEnd);
|
|
369
|
+
} else if (typeof value === "string") {
|
|
370
|
+
value = (value as string).slice(sStart ?? 0, sEnd);
|
|
371
|
+
} else {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
|
|
360
377
|
let key: string | number;
|
|
361
378
|
// Check if this part came from bracket access and needs variable resolution
|
|
362
|
-
if (
|
|
363
|
-
(part.startsWith("'") && part.endsWith("'"))) {
|
|
379
|
+
if (isQuotedPart) {
|
|
364
380
|
// Quoted string literal — strip quotes
|
|
365
381
|
key = part.slice(1, -1);
|
|
366
382
|
} else {
|
|
@@ -369,7 +385,7 @@ function resolveVar(expr: string, context: Record<string, unknown>): unknown {
|
|
|
369
385
|
key = asNum;
|
|
370
386
|
} else if (isBracket) {
|
|
371
387
|
// Only resolve as a variable from context for bracket-derived parts
|
|
372
|
-
const resolved = context
|
|
388
|
+
const resolved = evalExpr(part, context);
|
|
373
389
|
key = resolved !== undefined ? String(resolved) : part;
|
|
374
390
|
} else {
|
|
375
391
|
// Dot-derived parts or root — use the part name directly as the key
|
|
@@ -397,10 +413,11 @@ function resolveVar(expr: string, context: Record<string, unknown>): unknown {
|
|
|
397
413
|
function findOutsideQuotes(expr: string, needle: string): number {
|
|
398
414
|
let inQuote: string | null = null;
|
|
399
415
|
let depth = 0;
|
|
416
|
+
let bracketDepth = 0;
|
|
400
417
|
let i = 0;
|
|
401
418
|
while (i <= expr.length - needle.length) {
|
|
402
419
|
const ch = expr[i];
|
|
403
|
-
if ((ch === '"' || ch === "'") && depth === 0) {
|
|
420
|
+
if ((ch === '"' || ch === "'") && depth === 0 && bracketDepth === 0) {
|
|
404
421
|
if (inQuote === null) {
|
|
405
422
|
inQuote = ch;
|
|
406
423
|
} else if (ch === inQuote) {
|
|
@@ -412,7 +429,9 @@ function findOutsideQuotes(expr: string, needle: string): number {
|
|
|
412
429
|
if (inQuote) { i++; continue; }
|
|
413
430
|
if (ch === "(") depth++;
|
|
414
431
|
else if (ch === ")") depth--;
|
|
415
|
-
if (
|
|
432
|
+
else if (ch === "[") bracketDepth++;
|
|
433
|
+
else if (ch === "]") bracketDepth--;
|
|
434
|
+
if (depth === 0 && bracketDepth === 0 && expr.slice(i, i + needle.length) === needle) {
|
|
416
435
|
return i;
|
|
417
436
|
}
|
|
418
437
|
i++;
|
|
@@ -425,10 +444,11 @@ function splitOutsideQuotes(expr: string, sep: string): string[] {
|
|
|
425
444
|
let currentStart = 0;
|
|
426
445
|
let inQuote: string | null = null;
|
|
427
446
|
let depth = 0;
|
|
447
|
+
let bracketDepth = 0;
|
|
428
448
|
let i = 0;
|
|
429
449
|
while (i <= expr.length - sep.length) {
|
|
430
450
|
const ch = expr[i];
|
|
431
|
-
if ((ch === '"' || ch === "'") && depth === 0) {
|
|
451
|
+
if ((ch === '"' || ch === "'") && depth === 0 && bracketDepth === 0) {
|
|
432
452
|
if (inQuote === null) {
|
|
433
453
|
inQuote = ch;
|
|
434
454
|
} else if (ch === inQuote) {
|
|
@@ -440,7 +460,9 @@ function splitOutsideQuotes(expr: string, sep: string): string[] {
|
|
|
440
460
|
if (inQuote) { i++; continue; }
|
|
441
461
|
if (ch === "(") depth++;
|
|
442
462
|
else if (ch === ")") depth--;
|
|
443
|
-
if (
|
|
463
|
+
else if (ch === "[") bracketDepth++;
|
|
464
|
+
else if (ch === "]") bracketDepth--;
|
|
465
|
+
if (depth === 0 && bracketDepth === 0 && expr.slice(i, i + sep.length) === sep) {
|
|
444
466
|
parts.push(expr.slice(currentStart, i));
|
|
445
467
|
i += sep.length;
|
|
446
468
|
currentStart = i;
|
|
@@ -1446,10 +1468,46 @@ export class Frond {
|
|
|
1446
1468
|
|
|
1447
1469
|
private extractBlocks(source: string): Record<string, string> {
|
|
1448
1470
|
const blocks: Record<string, string> = {};
|
|
1449
|
-
const
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1471
|
+
const blockOpen = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}/g;
|
|
1472
|
+
const blockClose = /\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
1473
|
+
|
|
1474
|
+
let pos = 0;
|
|
1475
|
+
while (pos < source.length) {
|
|
1476
|
+
blockOpen.lastIndex = pos;
|
|
1477
|
+
const mOpen = blockOpen.exec(source);
|
|
1478
|
+
if (!mOpen) break;
|
|
1479
|
+
|
|
1480
|
+
const name = mOpen[1];
|
|
1481
|
+
const contentStart = mOpen.index + mOpen[0].length;
|
|
1482
|
+
let depth = 1;
|
|
1483
|
+
let scan = contentStart;
|
|
1484
|
+
|
|
1485
|
+
while (depth > 0 && scan < source.length) {
|
|
1486
|
+
blockOpen.lastIndex = scan;
|
|
1487
|
+
blockClose.lastIndex = scan;
|
|
1488
|
+
const nextOpen = blockOpen.exec(source);
|
|
1489
|
+
const nextClose = blockClose.exec(source);
|
|
1490
|
+
|
|
1491
|
+
if (!nextClose) break; // malformed — no matching endblock
|
|
1492
|
+
|
|
1493
|
+
if (nextOpen && nextOpen.index < nextClose.index) {
|
|
1494
|
+
depth++;
|
|
1495
|
+
scan = nextOpen.index + nextOpen[0].length;
|
|
1496
|
+
} else {
|
|
1497
|
+
depth--;
|
|
1498
|
+
if (depth === 0) {
|
|
1499
|
+
blocks[name] = source.slice(contentStart, nextClose.index);
|
|
1500
|
+
pos = nextClose.index + nextClose[0].length;
|
|
1501
|
+
break;
|
|
1502
|
+
}
|
|
1503
|
+
scan = nextClose.index + nextClose[0].length;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (depth > 0) {
|
|
1508
|
+
// malformed, skip forward
|
|
1509
|
+
pos = contentStart;
|
|
1510
|
+
}
|
|
1453
1511
|
}
|
|
1454
1512
|
return blocks;
|
|
1455
1513
|
}
|
|
@@ -1459,6 +1517,40 @@ export class Frond {
|
|
|
1459
1517
|
context: Record<string, unknown>,
|
|
1460
1518
|
childBlocks: Record<string, string>,
|
|
1461
1519
|
): string {
|
|
1520
|
+
// --- Multi-level extends: check if parent itself extends a grandparent ---
|
|
1521
|
+
const extendsMatch = parentSource.trimStart().match(/\{%[-\s]*extends\s+["'](.+?)["']\s*[-]?%\}/);
|
|
1522
|
+
if (extendsMatch) {
|
|
1523
|
+
const grandparentName = extendsMatch[1];
|
|
1524
|
+
const grandparentSource = this.load(grandparentName);
|
|
1525
|
+
|
|
1526
|
+
// Extract block defaults defined in the parent template
|
|
1527
|
+
const parentBlocks = this.extractBlocks(parentSource);
|
|
1528
|
+
|
|
1529
|
+
// Child blocks override parent blocks at the same name
|
|
1530
|
+
const mergedBlocks: Record<string, string> = { ...parentBlocks, ...childBlocks };
|
|
1531
|
+
|
|
1532
|
+
// Resolve nested blocks: if a block value contains {% block inner %} tags,
|
|
1533
|
+
// replace them with mergedBlocks values too
|
|
1534
|
+
const nestedBlockRe = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}([\s\S]*?)\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
1535
|
+
let changed = true;
|
|
1536
|
+
while (changed) {
|
|
1537
|
+
changed = false;
|
|
1538
|
+
for (const name of Object.keys(mergedBlocks)) {
|
|
1539
|
+
const resolved = mergedBlocks[name].replace(nestedBlockRe, (_m, innerName: string, innerDefault: string) => {
|
|
1540
|
+
return mergedBlocks[innerName] ?? innerDefault;
|
|
1541
|
+
});
|
|
1542
|
+
if (resolved !== mergedBlocks[name]) {
|
|
1543
|
+
mergedBlocks[name] = resolved;
|
|
1544
|
+
changed = true;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// Recurse up the chain (handles 3+, 4+, ... levels)
|
|
1550
|
+
return this.renderWithBlocks(grandparentSource, context, mergedBlocks);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// --- Leaf parent (no extends) — resolve blocks and render ---
|
|
1462
1554
|
const pattern = /\{%[-\s]*block\s+(\w+)\s*[-]?%\}([\s\S]*?)\{%[-\s]*endblock\s*[-]?%\}/g;
|
|
1463
1555
|
const engine = this;
|
|
1464
1556
|
|
|
@@ -4,6 +4,70 @@ import { getAdapter } from "./database.js";
|
|
|
4
4
|
import { buildQuery, parseQueryString } from "./query.js";
|
|
5
5
|
import { validate } from "./validation.js";
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Auto-CRUD — discovers ORM models and auto-generates REST endpoints.
|
|
9
|
+
*
|
|
10
|
+
* Generated endpoints per model:
|
|
11
|
+
* GET /api/{table} — list with pagination, filtering, sorting
|
|
12
|
+
* GET /api/{table}/{id} — get single record
|
|
13
|
+
* POST /api/{table} — create record
|
|
14
|
+
* PUT /api/{table}/{id} — update record
|
|
15
|
+
* DELETE /api/{table}/{id} — delete record
|
|
16
|
+
*/
|
|
17
|
+
export class AutoCrud {
|
|
18
|
+
private static registered: Map<string, DiscoveredModel> = new Map();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register a model for auto-CRUD.
|
|
22
|
+
*/
|
|
23
|
+
static register(model: DiscoveredModel, prefix: string = "/api"): void {
|
|
24
|
+
const tableName = model.definition.tableName;
|
|
25
|
+
if (!tableName) {
|
|
26
|
+
throw new Error(`AutoCrud: model has no tableName set.`);
|
|
27
|
+
}
|
|
28
|
+
AutoCrud.registered.set(tableName, model);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Discover models from the provided array and register them.
|
|
33
|
+
* (In Node.js, models are discovered by the server and passed in.)
|
|
34
|
+
*/
|
|
35
|
+
static discover(discoveredModels: DiscoveredModel[], prefix: string = "/api"): string[] {
|
|
36
|
+
const names: string[] = [];
|
|
37
|
+
for (const model of discoveredModels) {
|
|
38
|
+
AutoCrud.register(model, prefix);
|
|
39
|
+
names.push(model.definition.tableName);
|
|
40
|
+
}
|
|
41
|
+
return names;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Return all registered models.
|
|
46
|
+
*/
|
|
47
|
+
static models(): Map<string, DiscoveredModel> {
|
|
48
|
+
return new Map(AutoCrud.registered);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Clear all registered models (useful for testing).
|
|
53
|
+
*/
|
|
54
|
+
static clear(): void {
|
|
55
|
+
AutoCrud.registered.clear();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Generate route definitions for all registered models.
|
|
60
|
+
*/
|
|
61
|
+
static generateRoutes(): RouteDefinition[] {
|
|
62
|
+
const models = Array.from(AutoCrud.registered.values());
|
|
63
|
+
return generateCrudRoutes(models);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Generate CRUD route definitions for the given models.
|
|
69
|
+
* (Standalone function for backward compatibility.)
|
|
70
|
+
*/
|
|
7
71
|
export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[] {
|
|
8
72
|
const routes: RouteDefinition[] = [];
|
|
9
73
|
|
|
@@ -38,33 +102,28 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
|
|
|
38
102
|
},
|
|
39
103
|
handler: async (req: Tina4Request, res: Tina4Response) => {
|
|
40
104
|
const adapter = getAdapter();
|
|
41
|
-
const options = parseQueryString(req.query);
|
|
42
|
-
const { sql, countSql, params } = buildQuery(tableName, options, extraConditions);
|
|
43
105
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
106
|
+
// Parse query params for filtering / sorting / pagination
|
|
107
|
+
const qp = parseQueryString(req.query ?? {});
|
|
108
|
+
const { sql, countSql, params } = buildQuery(tableName, qp, extraConditions);
|
|
109
|
+
|
|
110
|
+
// params includes limit and offset at the end; countSql doesn't need them
|
|
111
|
+
const countParams = params.slice(0, -2);
|
|
112
|
+
const rows = adapter.query(sql, params);
|
|
47
113
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
114
|
+
const countRow = adapter.query(countSql, countParams);
|
|
115
|
+
const total = Number(countRow[0]?.total ?? 0);
|
|
116
|
+
const limit = qp.limit ?? 100;
|
|
117
|
+
const page = qp.page ?? 1;
|
|
52
118
|
|
|
53
119
|
res.json({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
page,
|
|
62
|
-
per_page: limit,
|
|
63
|
-
perPage: limit,
|
|
64
|
-
totalPages,
|
|
65
|
-
total_pages: totalPages,
|
|
66
|
-
// Legacy nested meta (kept for any clients that use it)
|
|
67
|
-
meta: { total, page, limit, totalPages },
|
|
120
|
+
data: rows,
|
|
121
|
+
meta: {
|
|
122
|
+
total,
|
|
123
|
+
page,
|
|
124
|
+
limit,
|
|
125
|
+
totalPages: Math.ceil(total / limit),
|
|
126
|
+
},
|
|
68
127
|
});
|
|
69
128
|
},
|
|
70
129
|
});
|
|
@@ -79,16 +138,19 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
|
|
|
79
138
|
},
|
|
80
139
|
handler: async (req: Tina4Request, res: Tina4Response) => {
|
|
81
140
|
const adapter = getAdapter();
|
|
141
|
+
|
|
82
142
|
const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
|
|
83
|
-
const
|
|
143
|
+
const rows = adapter.query(
|
|
84
144
|
`SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
|
|
85
145
|
[req.params.id],
|
|
86
146
|
);
|
|
87
|
-
|
|
147
|
+
|
|
148
|
+
if (rows.length === 0) {
|
|
88
149
|
res.status(404).json({ error: "Not Found", statusCode: 404 });
|
|
89
150
|
return;
|
|
90
151
|
}
|
|
91
|
-
|
|
152
|
+
|
|
153
|
+
res.json({ data: rows[0] });
|
|
92
154
|
},
|
|
93
155
|
});
|
|
94
156
|
|
|
@@ -101,47 +163,39 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
|
|
|
101
163
|
tags: [tableName],
|
|
102
164
|
},
|
|
103
165
|
handler: async (req: Tina4Request, res: Tina4Response) => {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
res.status(400).json({ error: "Request body is required", statusCode: 400 });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
166
|
+
const adapter = getAdapter();
|
|
167
|
+
const body = req.body as Record<string, unknown>;
|
|
109
168
|
|
|
110
|
-
|
|
169
|
+
// Validate against field definitions
|
|
170
|
+
const errors = validate(body, fields);
|
|
111
171
|
if (errors.length > 0) {
|
|
112
|
-
res.status(422).json({ error: "Validation failed",
|
|
172
|
+
res.status(422).json({ error: "Validation failed", errors });
|
|
113
173
|
return;
|
|
114
174
|
}
|
|
115
175
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const def = fields[key];
|
|
121
|
-
return def && !(def.primaryKey && def.autoIncrement);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Add is_deleted = 0 for soft delete models
|
|
125
|
-
if (softDelete) {
|
|
126
|
-
insertFields.push(["is_deleted", 0]);
|
|
176
|
+
// Map JS property names to DB column names
|
|
177
|
+
const dbBody: Record<string, unknown> = {};
|
|
178
|
+
for (const [key, value] of Object.entries(body)) {
|
|
179
|
+
dbBody[getDbCol(key)] = value;
|
|
127
180
|
}
|
|
128
181
|
|
|
129
|
-
const columns =
|
|
130
|
-
const
|
|
131
|
-
const
|
|
182
|
+
const columns = Object.keys(dbBody);
|
|
183
|
+
const values = Object.values(dbBody);
|
|
184
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
132
185
|
|
|
133
|
-
|
|
134
|
-
`INSERT INTO "${tableName}" (${columns}) VALUES (${placeholders})`,
|
|
186
|
+
adapter.execute(
|
|
187
|
+
`INSERT INTO "${tableName}" (${columns.map((c) => `"${c}"`).join(", ")}) VALUES (${placeholders})`,
|
|
135
188
|
values,
|
|
136
|
-
)
|
|
189
|
+
);
|
|
137
190
|
|
|
138
|
-
|
|
191
|
+
// Fetch the created record to include auto-generated fields (e.g. id)
|
|
192
|
+
const lastId = adapter.lastInsertId();
|
|
139
193
|
const created = adapter.query(
|
|
140
194
|
`SELECT * FROM "${tableName}" WHERE "${pkColumn}" = ?`,
|
|
141
|
-
[
|
|
195
|
+
[lastId],
|
|
142
196
|
);
|
|
143
197
|
|
|
144
|
-
res.status(201).json({ data: created[0] });
|
|
198
|
+
res.status(201).json({ data: created[0] ?? { ...body, [pkField]: lastId } });
|
|
145
199
|
},
|
|
146
200
|
});
|
|
147
201
|
|
|
@@ -154,21 +208,9 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
|
|
|
154
208
|
tags: [tableName],
|
|
155
209
|
},
|
|
156
210
|
handler: async (req: Tina4Request, res: Tina4Response) => {
|
|
157
|
-
const body = req.body as Record<string, unknown> | undefined;
|
|
158
|
-
if (!body || typeof body !== "object") {
|
|
159
|
-
res.status(400).json({ error: "Request body is required", statusCode: 400 });
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const errors = validate(body, fields, true);
|
|
164
|
-
if (errors.length > 0) {
|
|
165
|
-
res.status(422).json({ error: "Validation failed", statusCode: 422, errors });
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
211
|
const adapter = getAdapter();
|
|
212
|
+
const body = req.body as Record<string, unknown>;
|
|
170
213
|
|
|
171
|
-
// Check exists (respect soft delete)
|
|
172
214
|
const conditions = [`"${pkColumn}" = ?`, ...extraConditions];
|
|
173
215
|
const existing = adapter.query(
|
|
174
216
|
`SELECT * FROM "${tableName}" WHERE ${conditions.join(" AND ")}`,
|
|
@@ -179,18 +221,19 @@ export function generateCrudRoutes(models: DiscoveredModel[]): RouteDefinition[]
|
|
|
179
221
|
return;
|
|
180
222
|
}
|
|
181
223
|
|
|
182
|
-
//
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
return;
|
|
224
|
+
// Map JS property names to DB column names
|
|
225
|
+
const dbBody: Record<string, unknown> = {};
|
|
226
|
+
for (const [key, value] of Object.entries(body)) {
|
|
227
|
+
dbBody[getDbCol(key)] = value;
|
|
187
228
|
}
|
|
188
229
|
|
|
189
|
-
const
|
|
190
|
-
|
|
230
|
+
const setClauses = Object.keys(dbBody)
|
|
231
|
+
.map((col) => `"${col}" = ?`)
|
|
232
|
+
.join(", ");
|
|
233
|
+
const values = [...Object.values(dbBody), req.params.id];
|
|
191
234
|
|
|
192
235
|
adapter.execute(
|
|
193
|
-
`UPDATE "${tableName}" SET ${
|
|
236
|
+
`UPDATE "${tableName}" SET ${setClauses} WHERE "${pkColumn}" = ?`,
|
|
194
237
|
values,
|
|
195
238
|
);
|
|
196
239
|
|
|
@@ -201,7 +201,7 @@ export class BaseModel {
|
|
|
201
201
|
* @returns A QueryBuilder instance bound to this model's table and database.
|
|
202
202
|
*/
|
|
203
203
|
static query(): QueryBuilder {
|
|
204
|
-
return QueryBuilder.
|
|
204
|
+
return QueryBuilder.fromTable(this.tableName, this.getDb());
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
@@ -122,6 +122,11 @@ export class DatabaseResult implements Iterable<Record<string, unknown>> {
|
|
|
122
122
|
return this.records[Symbol.iterator]();
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** Total count — cross-framework parity with Python/Ruby. */
|
|
126
|
+
size(): number {
|
|
127
|
+
return this.count;
|
|
128
|
+
}
|
|
129
|
+
|
|
125
130
|
/** Number of records in this page. */
|
|
126
131
|
get length(): number {
|
|
127
132
|
return this.records.length;
|
|
@@ -35,7 +35,7 @@ export {
|
|
|
35
35
|
Migration,
|
|
36
36
|
} from "./migration.js";
|
|
37
37
|
export type { MigrationResult, MigrationStatus } from "./migration.js";
|
|
38
|
-
export { generateCrudRoutes } from "./autoCrud.js";
|
|
38
|
+
export { AutoCrud, generateCrudRoutes } from "./autoCrud.js";
|
|
39
39
|
export { buildQuery, parseQueryString } from "./query.js";
|
|
40
40
|
export { validate } from "./validation.js";
|
|
41
41
|
export type { ValidationError } from "./validation.js";
|
|
@@ -706,8 +706,11 @@ export async function status(
|
|
|
706
706
|
*/
|
|
707
707
|
export async function createMigration(
|
|
708
708
|
description: string,
|
|
709
|
-
options?: { migrationsDir?: string },
|
|
710
|
-
): Promise<{ upPath: string; downPath: string }> {
|
|
709
|
+
options?: { migrationsDir?: string; kind?: "sql" | "class" },
|
|
710
|
+
): Promise<string | { upPath: string; downPath: string }> {
|
|
711
|
+
if (options?.kind === "class") {
|
|
712
|
+
return createClassMigration(description, options);
|
|
713
|
+
}
|
|
711
714
|
const dir = resolve(options?.migrationsDir ?? "migrations");
|
|
712
715
|
|
|
713
716
|
// Ensure directory exists
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Usage:
|
|
5
5
|
* // Standalone
|
|
6
|
-
* const result = QueryBuilder.
|
|
6
|
+
* const result = QueryBuilder.fromTable("users", db)
|
|
7
7
|
* .select("id", "name")
|
|
8
8
|
* .where("active = ?", [1])
|
|
9
9
|
* .orderBy("name ASC")
|
|
@@ -49,7 +49,7 @@ export class QueryBuilder {
|
|
|
49
49
|
* @param db - Optional database adapter.
|
|
50
50
|
* @returns A new QueryBuilder instance.
|
|
51
51
|
*/
|
|
52
|
-
static
|
|
52
|
+
static fromTable(tableName: string, db?: DatabaseAdapter): QueryBuilder {
|
|
53
53
|
return new QueryBuilder(tableName, db);
|
|
54
54
|
}
|
|
55
55
|
|
|
@@ -8,9 +8,9 @@ interface OpenAPISpec {
|
|
|
8
8
|
components?: { schemas?: Record<string, unknown> };
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export function
|
|
11
|
+
export function generate(
|
|
12
12
|
routes: RouteDefinition[],
|
|
13
|
-
models: ModelDefinition[]
|
|
13
|
+
models: ModelDefinition[] = []
|
|
14
14
|
): OpenAPISpec {
|
|
15
15
|
const spec: OpenAPISpec = {
|
|
16
16
|
openapi: "3.0.3",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { generate } from "./generator.js";
|
|
2
2
|
export { createSwaggerRoutes } from "./ui.js";
|