tina4-nodejs 3.10.38 → 3.10.40

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tina4-nodejs",
3
- "version": "3.10.38",
3
+ "version": "3.10.40",
4
4
  "type": "module",
5
5
  "description": "This is not a framework. Tina4 for Node.js/TypeScript — zero deps, 38 built-in features.",
6
6
  "keywords": ["tina4", "framework", "web", "api", "orm", "graphql", "websocket", "typescript"],
@@ -7,6 +7,7 @@ import { migrateRollback } from "./commands/migrateRollback.js";
7
7
  import { listRoutes } from "./commands/routes.js";
8
8
  import { runTests } from "./commands/test.js";
9
9
  import { generate } from "./commands/generate.js";
10
+ import { runSeeds } from "./commands/seed.js";
10
11
 
11
12
  const args = process.argv.slice(2);
12
13
  const command = args[0];
@@ -24,6 +25,7 @@ const HELP = `
24
25
  tina4nodejs routes List all registered routes
25
26
  tina4nodejs test [file] Run project tests
26
27
  tina4nodejs generate <what> <name> Generate scaffolding (model, route, migration, middleware)
28
+ tina4nodejs seed [file] Run database seed files from src/seeds/
27
29
  tina4nodejs ai Install AI coding assistant context files
28
30
  tina4nodejs help Show this help message
29
31
 
@@ -78,6 +80,10 @@ async function main(): Promise<void> {
78
80
  await generate(what, genName);
79
81
  break;
80
82
  }
83
+ case "seed": {
84
+ await runSeeds(args[1]);
85
+ break;
86
+ }
81
87
  case "ai": {
82
88
  const { showMenu, installSelected, installAll } = await import("../../core/src/ai.js");
83
89
  const root = args[1] || ".";
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CLI command: seed — Run database seed files.
3
+ *
4
+ * Scans src/seeds/ for .ts files and executes them with tsx in alphabetical order.
5
+ */
6
+ import { existsSync, readdirSync } from "node:fs";
7
+ import { resolve, join } from "node:path";
8
+ import { execSync } from "node:child_process";
9
+
10
+ export async function runSeeds(seedPath?: string): Promise<void> {
11
+ const cwd = process.cwd();
12
+ const seedDir = resolve(cwd, "src/seeds");
13
+
14
+ // If a specific seed file is provided, run it directly
15
+ if (seedPath) {
16
+ const file = resolve(seedPath);
17
+ if (!existsSync(file)) {
18
+ console.error(` Error: Seed file not found: ${seedPath}`);
19
+ process.exit(1);
20
+ }
21
+ console.log(` Running seed: ${seedPath}\n`);
22
+ try {
23
+ execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
24
+ } catch {
25
+ process.exit(1);
26
+ }
27
+ return;
28
+ }
29
+
30
+ // Auto-discover seed files in src/seeds/
31
+ if (!existsSync(seedDir)) {
32
+ console.log(" No seeds directory found.");
33
+ console.log(" Create seed files in src/seeds/ (e.g. src/seeds/001-users.ts)");
34
+ return;
35
+ }
36
+
37
+ let seedFiles: string[];
38
+ try {
39
+ seedFiles = readdirSync(seedDir)
40
+ .filter((f) => f.endsWith(".ts"))
41
+ .sort()
42
+ .map((f) => join(seedDir, f));
43
+ } catch {
44
+ console.log(" Could not read src/seeds/ directory.");
45
+ return;
46
+ }
47
+
48
+ if (seedFiles.length === 0) {
49
+ console.log(" No seed files found in src/seeds/");
50
+ return;
51
+ }
52
+
53
+ console.log(` Found ${seedFiles.length} seed file(s)\n`);
54
+
55
+ let failed = false;
56
+ for (const file of seedFiles) {
57
+ const relative = file.replace(cwd + "/", "");
58
+ console.log(` Seeding: ${relative}`);
59
+ try {
60
+ execSync(`npx tsx "${file}"`, { cwd, stdio: "inherit" });
61
+ } catch {
62
+ failed = true;
63
+ }
64
+ }
65
+
66
+ if (failed) {
67
+ console.error("\n Some seeds failed.");
68
+ process.exit(1);
69
+ }
70
+
71
+ console.log("\n All seeds completed.");
72
+ }
@@ -2332,6 +2332,8 @@ function tina4VersionModal(){
2332
2332
  if(l>c){isNewer=true;break;}
2333
2333
  if(l<c)break;
2334
2334
  }
2335
+ var isAhead=false;
2336
+ if(!isNewer){for(var i=0;i<Math.max(cParts.length,lParts.length);i++){var c2=cParts[i]||0,l2=lParts[i]||0;if(c2>l2){isAhead=true;break;}if(c2<l2)break;}}
2335
2337
  if(isNewer){
2336
2338
  var breaking=(lParts[0]!==cParts[0]||lParts[1]!==cParts[1]);
2337
2339
  el.innerHTML='Latest: <strong style="color:#f9e2af;">v'+latest+'</strong>';
@@ -2340,6 +2342,9 @@ function tina4VersionModal(){
2340
2342
  }else{
2341
2343
  el.innerHTML+='<div style="color:#f9e2af;margin-top:6px;">Patch update available. Run: <code style="background:#313244;padding:2px 6px;border-radius:3px;">npm install tina4-nodejs@latest</code></div>';
2342
2344
  }
2345
+ }else if(isAhead){
2346
+ el.innerHTML='You are running <strong style="color:#cba6f7;">v'+current+'</strong> (ahead of npm <strong>v'+latest+'</strong> &mdash; not yet published).';
2347
+ el.style.color='#cba6f7';
2343
2348
  }else{
2344
2349
  el.innerHTML='Latest: <strong style="color:#a6e3a1;">v'+latest+'</strong> &mdash; You are up to date!';
2345
2350
  el.style.color='#a6e3a1';
@@ -247,6 +247,11 @@ export class Router {
247
247
  return all;
248
248
  }
249
249
 
250
+ /** Alias for getRoutes(). */
251
+ allRoutes(): RouteDefinition[] {
252
+ return this.getRoutes();
253
+ }
254
+
250
255
  /**
251
256
  * List all routes in a debug-friendly format for CLI output.
252
257
  */
@@ -188,6 +188,31 @@ export class BaseModel {
188
188
  return instance;
189
189
  }
190
190
 
191
+ /**
192
+ * Create a new instance from data, save it, and return the saved instance.
193
+ *
194
+ * Usage:
195
+ * const user = User.create({ name: "Alice", email: "alice@example.com" });
196
+ */
197
+ static create<T extends BaseModel>(
198
+ this: new (data?: Record<string, unknown>) => T,
199
+ data: Record<string, unknown>,
200
+ ): T {
201
+ const instance = new this(data) as T;
202
+ instance.save();
203
+ return instance;
204
+ }
205
+
206
+ /** Alias for findById(). */
207
+ static find<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown, include?: string[]): T | null {
208
+ return (this as unknown as typeof BaseModel).findById.call(this, id, include) as T | null;
209
+ }
210
+
211
+ /** Alias for findById(). */
212
+ static load<T extends BaseModel>(this: new (data?: Record<string, unknown>) => T, id: unknown, include?: string[]): T | null {
213
+ return (this as unknown as typeof BaseModel).findById.call(this, id, include) as T | null;
214
+ }
215
+
191
216
  /**
192
217
  * Find all records, optionally with a where clause.
193
218
  * @param where Optional WHERE clause.
@@ -393,6 +393,44 @@ export class Database {
393
393
  return this.getNextAdapter().tables();
394
394
  }
395
395
 
396
+ /**
397
+ * Get column metadata for a table.
398
+ * Uses the adapter's columns() method which handles engine-specific introspection
399
+ * (PRAGMA table_info for SQLite, information_schema.columns for others).
400
+ *
401
+ * @param tableName - Name of the table to inspect.
402
+ * @returns Array of column info objects: { name, type, nullable, default, primaryKey }.
403
+ */
404
+ getColumns(tableName: string): { name: string; type: string; nullable?: boolean; default?: unknown; primaryKey?: boolean }[] {
405
+ return this.getNextAdapter().columns(tableName);
406
+ }
407
+
408
+ /**
409
+ * Execute a SQL statement with multiple parameter sets (batch insert/update).
410
+ * Wraps all executions in a single transaction for atomicity and performance.
411
+ *
412
+ * @param sql - The SQL statement with parameter placeholders.
413
+ * @param paramSets - Array of parameter arrays, one per execution.
414
+ * @returns Array of results from each execution.
415
+ */
416
+ executeMany(sql: string, paramSets: unknown[][]): unknown[] {
417
+ const adapter = this.getNextAdapter();
418
+ const results: unknown[] = [];
419
+
420
+ adapter.startTransaction();
421
+ try {
422
+ for (const params of paramSets) {
423
+ results.push(adapter.execute(sql, params));
424
+ }
425
+ adapter.commit();
426
+ } catch (e) {
427
+ adapter.rollback();
428
+ throw e;
429
+ }
430
+
431
+ return results;
432
+ }
433
+
396
434
  /** Get the last auto-increment id. */
397
435
  getLastId(): string | number {
398
436
  const id = this.getNextAdapter().lastInsertId();