tina4-nodejs 3.0.0-rc.2

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.
Files changed (119) hide show
  1. package/BENCHMARK_REPORT.md +96 -0
  2. package/CARBONAH.md +140 -0
  3. package/CLAUDE.md +599 -0
  4. package/COMPARISON.md +194 -0
  5. package/README.md +595 -0
  6. package/package.json +59 -0
  7. package/packages/cli/src/bin.ts +110 -0
  8. package/packages/cli/src/commands/init.ts +194 -0
  9. package/packages/cli/src/commands/migrate.ts +96 -0
  10. package/packages/cli/src/commands/migrateCreate.ts +59 -0
  11. package/packages/cli/src/commands/routes.ts +61 -0
  12. package/packages/cli/src/commands/serve.ts +58 -0
  13. package/packages/cli/src/commands/test.ts +83 -0
  14. package/packages/core/gallery/auth/meta.json +1 -0
  15. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
  16. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
  17. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
  18. package/packages/core/gallery/database/meta.json +1 -0
  19. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
  20. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
  21. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
  22. package/packages/core/gallery/error-overlay/meta.json +1 -0
  23. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
  24. package/packages/core/gallery/orm/meta.json +1 -0
  25. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
  26. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
  27. package/packages/core/gallery/queue/meta.json +1 -0
  28. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
  29. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
  30. package/packages/core/gallery/rest-api/meta.json +1 -0
  31. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
  32. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
  33. package/packages/core/gallery/templates/meta.json +1 -0
  34. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
  35. package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
  36. package/packages/core/public/css/tina4.css +2463 -0
  37. package/packages/core/public/css/tina4.min.css +1 -0
  38. package/packages/core/public/favicon.ico +0 -0
  39. package/packages/core/public/images/logo.svg +5 -0
  40. package/packages/core/public/images/tina4-logo-icon.webp +0 -0
  41. package/packages/core/public/js/frond.min.js +420 -0
  42. package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
  43. package/packages/core/public/js/tina4.min.js +93 -0
  44. package/packages/core/public/swagger/index.html +90 -0
  45. package/packages/core/public/swagger/oauth2-redirect.html +63 -0
  46. package/packages/core/src/ai.ts +359 -0
  47. package/packages/core/src/api.ts +248 -0
  48. package/packages/core/src/auth.ts +287 -0
  49. package/packages/core/src/cache.ts +121 -0
  50. package/packages/core/src/constants.ts +48 -0
  51. package/packages/core/src/container.ts +90 -0
  52. package/packages/core/src/devAdmin.ts +2024 -0
  53. package/packages/core/src/devMailbox.ts +316 -0
  54. package/packages/core/src/dotenv.ts +172 -0
  55. package/packages/core/src/errorOverlay.test.ts +122 -0
  56. package/packages/core/src/errorOverlay.ts +278 -0
  57. package/packages/core/src/events.ts +112 -0
  58. package/packages/core/src/fakeData.ts +309 -0
  59. package/packages/core/src/graphql.ts +812 -0
  60. package/packages/core/src/health.ts +31 -0
  61. package/packages/core/src/htmlElement.ts +172 -0
  62. package/packages/core/src/i18n.ts +136 -0
  63. package/packages/core/src/index.ts +88 -0
  64. package/packages/core/src/logger.ts +226 -0
  65. package/packages/core/src/messenger.ts +822 -0
  66. package/packages/core/src/middleware.ts +138 -0
  67. package/packages/core/src/queue.ts +481 -0
  68. package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
  69. package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
  70. package/packages/core/src/rateLimiter.ts +107 -0
  71. package/packages/core/src/request.ts +189 -0
  72. package/packages/core/src/response.ts +146 -0
  73. package/packages/core/src/routeDiscovery.ts +87 -0
  74. package/packages/core/src/router.ts +398 -0
  75. package/packages/core/src/scss.ts +366 -0
  76. package/packages/core/src/server.ts +610 -0
  77. package/packages/core/src/service.ts +380 -0
  78. package/packages/core/src/session.ts +480 -0
  79. package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
  80. package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
  81. package/packages/core/src/static.ts +58 -0
  82. package/packages/core/src/testing.ts +233 -0
  83. package/packages/core/src/types.ts +98 -0
  84. package/packages/core/src/watcher.ts +37 -0
  85. package/packages/core/src/websocket.ts +408 -0
  86. package/packages/core/src/wsdl.ts +546 -0
  87. package/packages/core/templates/errors/302.twig +14 -0
  88. package/packages/core/templates/errors/401.twig +9 -0
  89. package/packages/core/templates/errors/403.twig +29 -0
  90. package/packages/core/templates/errors/404.twig +29 -0
  91. package/packages/core/templates/errors/500.twig +38 -0
  92. package/packages/core/templates/errors/502.twig +9 -0
  93. package/packages/core/templates/errors/503.twig +12 -0
  94. package/packages/core/templates/errors/base.twig +37 -0
  95. package/packages/frond/src/engine.ts +1475 -0
  96. package/packages/frond/src/index.ts +2 -0
  97. package/packages/orm/src/adapters/firebird.ts +455 -0
  98. package/packages/orm/src/adapters/mssql.ts +440 -0
  99. package/packages/orm/src/adapters/mysql.ts +355 -0
  100. package/packages/orm/src/adapters/postgres.ts +362 -0
  101. package/packages/orm/src/adapters/sqlite.ts +270 -0
  102. package/packages/orm/src/autoCrud.ts +231 -0
  103. package/packages/orm/src/baseModel.ts +536 -0
  104. package/packages/orm/src/database.ts +321 -0
  105. package/packages/orm/src/fakeData.ts +118 -0
  106. package/packages/orm/src/index.ts +49 -0
  107. package/packages/orm/src/migration.ts +392 -0
  108. package/packages/orm/src/model.ts +56 -0
  109. package/packages/orm/src/query.ts +113 -0
  110. package/packages/orm/src/seeder.ts +120 -0
  111. package/packages/orm/src/sqlTranslation.ts +272 -0
  112. package/packages/orm/src/types.ts +110 -0
  113. package/packages/orm/src/validation.ts +93 -0
  114. package/packages/swagger/src/generator.ts +189 -0
  115. package/packages/swagger/src/index.ts +2 -0
  116. package/packages/swagger/src/ui.ts +48 -0
  117. package/skills/tina4-developer.skill +0 -0
  118. package/skills/tina4-js.skill +0 -0
  119. package/skills/tina4-maintainer.skill +0 -0
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Tina4 PostgreSQL Adapter — uses the `pg` package (optional peer dependency).
3
+ *
4
+ * Install: npm install pg @types/pg
5
+ * URL format: postgresql://user:pass@host:port/database
6
+ */
7
+ import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
8
+ import { SQLTranslator } from "../sqlTranslation.js";
9
+
10
+ let pg: typeof import("pg") | null = null;
11
+
12
+ function requirePg(): typeof import("pg") {
13
+ if (pg) return pg;
14
+ try {
15
+ // Dynamic require via createRequire for ESM compatibility
16
+ const { createRequire } = await_import_module();
17
+ const require = createRequire(import.meta.url);
18
+ pg = require("pg");
19
+ return pg!;
20
+ } catch {
21
+ throw new Error(
22
+ 'PostgreSQL adapter requires the "pg" package. Install it with: npm install pg',
23
+ );
24
+ }
25
+ }
26
+
27
+ /** Synchronous helper to get createRequire — we call this at connection time. */
28
+ function await_import_module() {
29
+ // node:module is always available
30
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
31
+ return require("node:module") as typeof import("node:module");
32
+ }
33
+
34
+ export interface PostgresConfig {
35
+ host?: string;
36
+ port?: number;
37
+ user?: string;
38
+ password?: string;
39
+ database?: string;
40
+ connectionString?: string;
41
+ }
42
+
43
+ export class PostgresAdapter implements DatabaseAdapter {
44
+ private client: InstanceType<typeof import("pg").Client> | null = null;
45
+ private _lastInsertId: number | bigint | null = null;
46
+
47
+ constructor(private config: PostgresConfig | string) {}
48
+
49
+ /** Connect to PostgreSQL. Must be called before using the adapter. */
50
+ async connect(): Promise<void> {
51
+ const pgModule = requirePg();
52
+ const Client = pgModule.Client ?? (pgModule as any).default?.Client;
53
+
54
+ if (typeof this.config === "string") {
55
+ this.client = new Client({ connectionString: this.config });
56
+ } else {
57
+ this.client = new Client(this.config);
58
+ }
59
+
60
+ await this.client!.connect();
61
+ }
62
+
63
+ private ensureConnected(): asserts this is { client: NonNullable<PostgresAdapter["client"]> } {
64
+ if (!this.client) {
65
+ throw new Error("PostgreSQL adapter not connected. Call connect() first.");
66
+ }
67
+ }
68
+
69
+ /** Convert ? placeholders to $1, $2, ... for pg. */
70
+ private convertPlaceholders(sql: string): string {
71
+ let count = 0;
72
+ return sql.replace(/\?/g, () => {
73
+ count++;
74
+ return `$${count}`;
75
+ });
76
+ }
77
+
78
+ execute(sql: string, params?: unknown[]): unknown {
79
+ this.ensureConnected();
80
+ const convertedSql = this.convertPlaceholders(sql);
81
+ // pg client methods are async — we store a promise-based wrapper
82
+ // Since the interface is sync, we provide executeAsync for real usage
83
+ throw new Error("Use executeAsync() for PostgreSQL — async adapter requires async methods.");
84
+ }
85
+
86
+ executeMany(sql: string, paramsList: unknown[][]): { totalAffected: number; lastInsertId?: number | bigint } {
87
+ throw new Error("Use executeManyAsync() for PostgreSQL — async adapter requires async methods.");
88
+ }
89
+
90
+ /** Async executeMany for real usage. */
91
+ async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
92
+ let totalAffected = 0;
93
+ let lastId: number | bigint | undefined;
94
+ for (const params of paramsList) {
95
+ const result = await this.executeAsync(sql, params);
96
+ totalAffected++;
97
+ if (result && typeof result === "object" && "lastInsertId" in (result as any)) {
98
+ lastId = (result as any).lastInsertId;
99
+ }
100
+ }
101
+ return { totalAffected, lastInsertId: lastId };
102
+ }
103
+
104
+ /** Async execute for real usage. */
105
+ async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
106
+ this.ensureConnected();
107
+ const convertedSql = this.convertPlaceholders(sql);
108
+ const result = await this.client!.query(convertedSql, params);
109
+ if (result.rows?.[0]?.id !== undefined) {
110
+ this._lastInsertId = result.rows[0].id;
111
+ }
112
+ return result;
113
+ }
114
+
115
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
116
+ throw new Error("Use queryAsync() for PostgreSQL — async adapter requires async methods.");
117
+ }
118
+
119
+ /** Async query for real usage. */
120
+ async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
121
+ this.ensureConnected();
122
+ const convertedSql = this.convertPlaceholders(sql);
123
+ const result = await this.client!.query(convertedSql, params);
124
+ return result.rows as T[];
125
+ }
126
+
127
+ fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
128
+ throw new Error("Use fetchAsync() for PostgreSQL.");
129
+ }
130
+
131
+ async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
132
+ let effectiveSql = sql;
133
+ if (limit !== undefined) {
134
+ effectiveSql += ` LIMIT ${limit}`;
135
+ if (skip !== undefined && skip > 0) {
136
+ effectiveSql += ` OFFSET ${skip}`;
137
+ }
138
+ }
139
+ return this.queryAsync<T>(effectiveSql, params);
140
+ }
141
+
142
+ fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
143
+ throw new Error("Use fetchOneAsync() for PostgreSQL.");
144
+ }
145
+
146
+ async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
147
+ const rows = await this.queryAsync<T>(sql, params);
148
+ return rows[0] ?? null;
149
+ }
150
+
151
+ insert(table: string, data: Record<string, unknown>): DatabaseResult {
152
+ throw new Error("Use insertAsync() for PostgreSQL.");
153
+ }
154
+
155
+ async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
156
+ this.ensureConnected();
157
+ const keys = Object.keys(data);
158
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
159
+ const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders}) RETURNING *`;
160
+ const values = Object.values(data);
161
+
162
+ try {
163
+ const result = await this.client!.query(sql, values);
164
+ const insertedRow = result.rows[0];
165
+ const id = insertedRow?.id ?? null;
166
+ if (id !== null) this._lastInsertId = id;
167
+ return {
168
+ success: true,
169
+ rowsAffected: result.rowCount ?? 1,
170
+ lastInsertId: id,
171
+ };
172
+ } catch (e) {
173
+ return { success: false, rowsAffected: 0, error: (e as Error).message };
174
+ }
175
+ }
176
+
177
+ update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): DatabaseResult {
178
+ throw new Error("Use updateAsync() for PostgreSQL.");
179
+ }
180
+
181
+ async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
182
+ this.ensureConnected();
183
+ const dataKeys = Object.keys(data);
184
+ const filterKeys = Object.keys(filter);
185
+ let paramIndex = 1;
186
+
187
+ const setClauses = dataKeys.map((k) => `"${k}" = $${paramIndex++}`).join(", ");
188
+ const whereClauses = filterKeys.map((k) => `"${k}" = $${paramIndex++}`).join(" AND ");
189
+ const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
190
+ const values = [...Object.values(data), ...Object.values(filter)];
191
+
192
+ try {
193
+ const result = await this.client!.query(sql, values);
194
+ return { success: true, rowsAffected: result.rowCount ?? 0 };
195
+ } catch (e) {
196
+ return { success: false, rowsAffected: 0, error: (e as Error).message };
197
+ }
198
+ }
199
+
200
+ delete(table: string, filter: Record<string, unknown>): DatabaseResult {
201
+ throw new Error("Use deleteAsync() for PostgreSQL.");
202
+ }
203
+
204
+ async deleteAsync(table: string, filter: Record<string, unknown>): Promise<DatabaseResult> {
205
+ this.ensureConnected();
206
+ const filterKeys = Object.keys(filter);
207
+ let paramIndex = 1;
208
+ const whereClauses = filterKeys.map((k) => `"${k}" = $${paramIndex++}`).join(" AND ");
209
+ const sql = `DELETE FROM "${table}" WHERE ${whereClauses}`;
210
+ const values = Object.values(filter);
211
+
212
+ try {
213
+ const result = await this.client!.query(sql, values);
214
+ return { success: true, rowsAffected: result.rowCount ?? 0 };
215
+ } catch (e) {
216
+ return { success: false, rowsAffected: 0, error: (e as Error).message };
217
+ }
218
+ }
219
+
220
+ startTransaction(): void {
221
+ throw new Error("Use startTransactionAsync() for PostgreSQL.");
222
+ }
223
+
224
+ async startTransactionAsync(): Promise<void> {
225
+ await this.executeAsync("BEGIN");
226
+ }
227
+
228
+ commit(): void {
229
+ throw new Error("Use commitAsync() for PostgreSQL.");
230
+ }
231
+
232
+ async commitAsync(): Promise<void> {
233
+ await this.executeAsync("COMMIT");
234
+ }
235
+
236
+ rollback(): void {
237
+ throw new Error("Use rollbackAsync() for PostgreSQL.");
238
+ }
239
+
240
+ async rollbackAsync(): Promise<void> {
241
+ await this.executeAsync("ROLLBACK");
242
+ }
243
+
244
+ tables(): string[] {
245
+ throw new Error("Use tablesAsync() for PostgreSQL.");
246
+ }
247
+
248
+ async tablesAsync(): Promise<string[]> {
249
+ const rows = await this.queryAsync<{ tablename: string }>(
250
+ "SELECT tablename FROM pg_tables WHERE schemaname = 'public'",
251
+ );
252
+ return rows.map((r) => r.tablename);
253
+ }
254
+
255
+ columns(table: string): ColumnInfo[] {
256
+ throw new Error("Use columnsAsync() for PostgreSQL.");
257
+ }
258
+
259
+ async columnsAsync(table: string): Promise<ColumnInfo[]> {
260
+ const rows = await this.queryAsync<{
261
+ column_name: string;
262
+ data_type: string;
263
+ is_nullable: string;
264
+ column_default: string | null;
265
+ }>(
266
+ "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1",
267
+ [table],
268
+ );
269
+ return rows.map((r) => ({
270
+ name: r.column_name,
271
+ type: r.data_type,
272
+ nullable: r.is_nullable === "YES",
273
+ default: r.column_default,
274
+ primaryKey: false,
275
+ }));
276
+ }
277
+
278
+ lastInsertId(): number | bigint | null {
279
+ return this._lastInsertId;
280
+ }
281
+
282
+ close(): void {
283
+ if (this.client) {
284
+ this.client.end();
285
+ this.client = null;
286
+ }
287
+ }
288
+
289
+ tableExists(name: string): boolean {
290
+ throw new Error("Use tableExistsAsync() for PostgreSQL.");
291
+ }
292
+
293
+ async tableExistsAsync(name: string): Promise<boolean> {
294
+ const row = await this.fetchOneAsync<{ exists: boolean }>(
295
+ "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = $1) AS exists",
296
+ [name],
297
+ );
298
+ return row?.exists ?? false;
299
+ }
300
+
301
+ createTable(name: string, columns: Record<string, FieldDefinition>): void {
302
+ throw new Error("Use createTableAsync() for PostgreSQL.");
303
+ }
304
+
305
+ async createTableAsync(name: string, columns: Record<string, FieldDefinition>): Promise<void> {
306
+ const colDefs: string[] = [];
307
+
308
+ for (const [colName, def] of Object.entries(columns)) {
309
+ const sqlType = fieldTypeToPostgres(def);
310
+ const parts = [`"${colName}" ${sqlType}`];
311
+
312
+ if (def.primaryKey && !def.autoIncrement) parts.push("PRIMARY KEY");
313
+ if (def.required && !def.primaryKey) parts.push("NOT NULL");
314
+ if (def.default !== undefined && def.default !== "now") {
315
+ parts.push(`DEFAULT ${sqlDefault(def.default)}`);
316
+ }
317
+ if (def.default === "now") {
318
+ parts.push("DEFAULT CURRENT_TIMESTAMP");
319
+ }
320
+
321
+ colDefs.push(parts.join(" "));
322
+ }
323
+
324
+ const sql = `CREATE TABLE IF NOT EXISTS "${name}" (${colDefs.join(", ")})`;
325
+ await this.executeAsync(sql);
326
+ }
327
+
328
+ /** Translate SQL for PostgreSQL dialect. */
329
+ translateSql(sql: string): string {
330
+ // PostgreSQL supports ILIKE natively, standard LIMIT/OFFSET — minimal translation needed
331
+ return sql;
332
+ }
333
+ }
334
+
335
+ function fieldTypeToPostgres(def: FieldDefinition): string {
336
+ if (def.primaryKey && def.autoIncrement) {
337
+ return "SERIAL PRIMARY KEY";
338
+ }
339
+ switch (def.type) {
340
+ case "integer":
341
+ return "INTEGER";
342
+ case "number":
343
+ case "numeric":
344
+ return "DOUBLE PRECISION";
345
+ case "boolean":
346
+ return "BOOLEAN";
347
+ case "datetime":
348
+ return "TIMESTAMP";
349
+ case "text":
350
+ return "TEXT";
351
+ case "string":
352
+ return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
353
+ default:
354
+ return "TEXT";
355
+ }
356
+ }
357
+
358
+ function sqlDefault(value: unknown): string {
359
+ if (typeof value === "string") return `'${value}'`;
360
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
361
+ return String(value);
362
+ }
@@ -0,0 +1,270 @@
1
+ import Database from "better-sqlite3";
2
+ import { mkdirSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
5
+
6
+ export class SQLiteAdapter implements DatabaseAdapter {
7
+ private db: Database.Database;
8
+ private _lastInsertId: number | bigint | null = null;
9
+
10
+ constructor(dbPath: string) {
11
+ // Create directory if needed
12
+ mkdirSync(dirname(dbPath), { recursive: true });
13
+
14
+ this.db = new Database(dbPath);
15
+ this.db.pragma("journal_mode = WAL");
16
+ this.db.pragma("foreign_keys = ON");
17
+ }
18
+
19
+ execute(sql: string, params?: unknown[]): unknown {
20
+ const stmt = this.db.prepare(sql);
21
+ const result = params ? stmt.run(...params) : stmt.run();
22
+ if (result && typeof result === "object" && "lastInsertRowid" in result) {
23
+ this._lastInsertId = result.lastInsertRowid as number | bigint;
24
+ }
25
+ return result;
26
+ }
27
+
28
+ executeMany(sql: string, paramsList: unknown[][]): { totalAffected: number; lastInsertId?: number | bigint } {
29
+ const stmt = this.db.prepare(sql);
30
+ let totalAffected = 0;
31
+ let lastId: number | bigint | undefined;
32
+
33
+ const runMany = this.db.transaction((rows: unknown[][]) => {
34
+ for (const params of rows) {
35
+ const result = stmt.run(...params);
36
+ totalAffected += result.changes;
37
+ if (result.lastInsertRowid) {
38
+ lastId = result.lastInsertRowid;
39
+ this._lastInsertId = result.lastInsertRowid;
40
+ }
41
+ }
42
+ });
43
+
44
+ runMany(paramsList);
45
+ return { totalAffected, lastInsertId: lastId };
46
+ }
47
+
48
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
49
+ const stmt = this.db.prepare(sql);
50
+ return (params ? stmt.all(...params) : stmt.all()) as T[];
51
+ }
52
+
53
+ fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
54
+ let effectiveSql = sql;
55
+ if (limit !== undefined) {
56
+ effectiveSql += ` LIMIT ${limit}`;
57
+ if (skip !== undefined && skip > 0) {
58
+ effectiveSql += ` OFFSET ${skip}`;
59
+ }
60
+ }
61
+ return this.query<T>(effectiveSql, params);
62
+ }
63
+
64
+ fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
65
+ const stmt = this.db.prepare(sql);
66
+ const row = params ? stmt.get(...params) : stmt.get();
67
+ return (row as T) ?? null;
68
+ }
69
+
70
+ insert(table: string, data: Record<string, unknown> | Record<string, unknown>[]): DatabaseResult {
71
+ // Handle list of rows — batch insert
72
+ if (Array.isArray(data)) {
73
+ if (data.length === 0) return { success: true, rowsAffected: 0 };
74
+ const keys = Object.keys(data[0]);
75
+ const placeholders = keys.map(() => "?").join(", ");
76
+ const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
77
+ const paramsList = data.map((row) => keys.map((k) => row[k]));
78
+ const result = this.executeMany(sql, paramsList);
79
+ return { success: true, rowsAffected: result.totalAffected, lastInsertId: result.lastInsertId };
80
+ }
81
+
82
+ const keys = Object.keys(data);
83
+ const placeholders = keys.map(() => "?").join(", ");
84
+ const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
85
+ const values = Object.values(data);
86
+
87
+ try {
88
+ const result = this.db.prepare(sql).run(...values);
89
+ this._lastInsertId = result.lastInsertRowid;
90
+ return {
91
+ success: true,
92
+ rowsAffected: result.changes,
93
+ lastInsertId: result.lastInsertRowid,
94
+ };
95
+ } catch (e) {
96
+ return {
97
+ success: false,
98
+ rowsAffected: 0,
99
+ error: (e as Error).message,
100
+ };
101
+ }
102
+ }
103
+
104
+ update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): DatabaseResult {
105
+ const setClauses = Object.keys(data).map((k) => `"${k}" = ?`).join(", ");
106
+ const whereClauses = Object.keys(filter).map((k) => `"${k}" = ?`).join(" AND ");
107
+ const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
108
+ const values = [...Object.values(data), ...Object.values(filter)];
109
+
110
+ try {
111
+ const result = this.db.prepare(sql).run(...values);
112
+ return { success: true, rowsAffected: result.changes };
113
+ } catch (e) {
114
+ return { success: false, rowsAffected: 0, error: (e as Error).message };
115
+ }
116
+ }
117
+
118
+ delete(table: string, filter: Record<string, unknown> | string | Record<string, unknown>[]): DatabaseResult {
119
+ // Array of objects — delete each row
120
+ if (Array.isArray(filter)) {
121
+ let totalAffected = 0;
122
+ for (const row of filter) {
123
+ const result = this.delete(table, row);
124
+ totalAffected += result.rowsAffected;
125
+ }
126
+ return { success: true, rowsAffected: totalAffected };
127
+ }
128
+
129
+ // String filter — raw WHERE clause
130
+ if (typeof filter === "string") {
131
+ const sql = filter ? `DELETE FROM "${table}" WHERE ${filter}` : `DELETE FROM "${table}"`;
132
+ try {
133
+ const result = this.db.prepare(sql).run();
134
+ return { success: true, rowsAffected: result.changes };
135
+ } catch (e) {
136
+ return { success: false, rowsAffected: 0, error: (e as Error).message };
137
+ }
138
+ }
139
+
140
+ // Object filter — build WHERE from keys
141
+ const whereClauses = Object.keys(filter).map((k) => `"${k}" = ?`).join(" AND ");
142
+ const sql = `DELETE FROM "${table}" WHERE ${whereClauses}`;
143
+ const values = Object.values(filter);
144
+
145
+ try {
146
+ const result = this.db.prepare(sql).run(...values);
147
+ return { success: true, rowsAffected: result.changes };
148
+ } catch (e) {
149
+ return { success: false, rowsAffected: 0, error: (e as Error).message };
150
+ }
151
+ }
152
+
153
+ startTransaction(): void {
154
+ this.db.exec("BEGIN TRANSACTION");
155
+ }
156
+
157
+ commit(): void {
158
+ this.db.exec("COMMIT");
159
+ }
160
+
161
+ rollback(): void {
162
+ this.db.exec("ROLLBACK");
163
+ }
164
+
165
+ tables(): string[] {
166
+ const rows = this.query<{ name: string }>(
167
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
168
+ );
169
+ return rows.map((r) => r.name);
170
+ }
171
+
172
+ columns(table: string): ColumnInfo[] {
173
+ const rows = this.db.prepare(`PRAGMA table_info("${table}")`).all() as Array<{
174
+ name: string;
175
+ type: string;
176
+ notnull: number;
177
+ dflt_value: unknown;
178
+ pk: number;
179
+ }>;
180
+ return rows.map((r) => ({
181
+ name: r.name,
182
+ type: r.type,
183
+ nullable: r.notnull === 0,
184
+ default: r.dflt_value,
185
+ primaryKey: r.pk === 1,
186
+ }));
187
+ }
188
+
189
+ lastInsertId(): number | bigint | null {
190
+ return this._lastInsertId;
191
+ }
192
+
193
+ close(): void {
194
+ this.db.close();
195
+ }
196
+
197
+ tableExists(name: string): boolean {
198
+ const result = this.db
199
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?")
200
+ .get(name);
201
+ return !!result;
202
+ }
203
+
204
+ createTable(name: string, columns: Record<string, FieldDefinition>): void {
205
+ const colDefs: string[] = [];
206
+
207
+ for (const [colName, def] of Object.entries(columns)) {
208
+ const sqlType = fieldTypeToSQLite(def.type);
209
+ const parts = [`"${colName}" ${sqlType}`];
210
+
211
+ if (def.primaryKey) parts.push("PRIMARY KEY");
212
+ if (def.autoIncrement) parts.push("AUTOINCREMENT");
213
+ if (def.required && !def.primaryKey) parts.push("NOT NULL");
214
+ if (def.default !== undefined && def.default !== "now") {
215
+ parts.push(`DEFAULT ${sqlDefault(def.default)}`);
216
+ }
217
+ if (def.default === "now") {
218
+ parts.push("DEFAULT CURRENT_TIMESTAMP");
219
+ }
220
+
221
+ colDefs.push(parts.join(" "));
222
+ }
223
+
224
+ const sql = `CREATE TABLE IF NOT EXISTS "${name}" (${colDefs.join(", ")})`;
225
+ this.db.exec(sql);
226
+ }
227
+
228
+ getTableColumns(name: string): Array<{ name: string; type: string }> {
229
+ return this.db.prepare(`PRAGMA table_info("${name}")`).all() as Array<{
230
+ name: string;
231
+ type: string;
232
+ }>;
233
+ }
234
+
235
+ addColumn(table: string, colName: string, def: FieldDefinition): void {
236
+ const sqlType = fieldTypeToSQLite(def.type);
237
+ let sql = `ALTER TABLE "${table}" ADD COLUMN "${colName}" ${sqlType}`;
238
+ if (def.default !== undefined && def.default !== "now") {
239
+ sql += ` DEFAULT ${sqlDefault(def.default)}`;
240
+ } else if (def.default === "now") {
241
+ sql += " DEFAULT CURRENT_TIMESTAMP";
242
+ }
243
+ this.db.exec(sql);
244
+ }
245
+ }
246
+
247
+ function fieldTypeToSQLite(type: string): string {
248
+ switch (type) {
249
+ case "integer":
250
+ return "INTEGER";
251
+ case "number":
252
+ case "numeric":
253
+ return "REAL";
254
+ case "boolean":
255
+ return "INTEGER";
256
+ case "datetime":
257
+ return "TEXT";
258
+ case "text":
259
+ return "TEXT";
260
+ case "string":
261
+ default:
262
+ return "TEXT";
263
+ }
264
+ }
265
+
266
+ function sqlDefault(value: unknown): string {
267
+ if (typeof value === "string") return `'${value}'`;
268
+ if (typeof value === "boolean") return value ? "1" : "0";
269
+ return String(value);
270
+ }