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.
- package/BENCHMARK_REPORT.md +96 -0
- package/CARBONAH.md +140 -0
- package/CLAUDE.md +599 -0
- package/COMPARISON.md +194 -0
- package/README.md +595 -0
- package/package.json +59 -0
- package/packages/cli/src/bin.ts +110 -0
- package/packages/cli/src/commands/init.ts +194 -0
- package/packages/cli/src/commands/migrate.ts +96 -0
- package/packages/cli/src/commands/migrateCreate.ts +59 -0
- package/packages/cli/src/commands/routes.ts +61 -0
- package/packages/cli/src/commands/serve.ts +58 -0
- package/packages/cli/src/commands/test.ts +83 -0
- package/packages/core/gallery/auth/meta.json +1 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
- package/packages/core/gallery/database/meta.json +1 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
- package/packages/core/gallery/error-overlay/meta.json +1 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
- package/packages/core/gallery/orm/meta.json +1 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
- package/packages/core/gallery/queue/meta.json +1 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
- package/packages/core/gallery/rest-api/meta.json +1 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
- package/packages/core/gallery/templates/meta.json +1 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
- package/packages/core/public/css/tina4.css +2463 -0
- package/packages/core/public/css/tina4.min.css +1 -0
- package/packages/core/public/favicon.ico +0 -0
- package/packages/core/public/images/logo.svg +5 -0
- package/packages/core/public/images/tina4-logo-icon.webp +0 -0
- package/packages/core/public/js/frond.min.js +420 -0
- package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
- package/packages/core/public/js/tina4.min.js +93 -0
- package/packages/core/public/swagger/index.html +90 -0
- package/packages/core/public/swagger/oauth2-redirect.html +63 -0
- package/packages/core/src/ai.ts +359 -0
- package/packages/core/src/api.ts +248 -0
- package/packages/core/src/auth.ts +287 -0
- package/packages/core/src/cache.ts +121 -0
- package/packages/core/src/constants.ts +48 -0
- package/packages/core/src/container.ts +90 -0
- package/packages/core/src/devAdmin.ts +2024 -0
- package/packages/core/src/devMailbox.ts +316 -0
- package/packages/core/src/dotenv.ts +172 -0
- package/packages/core/src/errorOverlay.test.ts +122 -0
- package/packages/core/src/errorOverlay.ts +278 -0
- package/packages/core/src/events.ts +112 -0
- package/packages/core/src/fakeData.ts +309 -0
- package/packages/core/src/graphql.ts +812 -0
- package/packages/core/src/health.ts +31 -0
- package/packages/core/src/htmlElement.ts +172 -0
- package/packages/core/src/i18n.ts +136 -0
- package/packages/core/src/index.ts +88 -0
- package/packages/core/src/logger.ts +226 -0
- package/packages/core/src/messenger.ts +822 -0
- package/packages/core/src/middleware.ts +138 -0
- package/packages/core/src/queue.ts +481 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
- package/packages/core/src/rateLimiter.ts +107 -0
- package/packages/core/src/request.ts +189 -0
- package/packages/core/src/response.ts +146 -0
- package/packages/core/src/routeDiscovery.ts +87 -0
- package/packages/core/src/router.ts +398 -0
- package/packages/core/src/scss.ts +366 -0
- package/packages/core/src/server.ts +610 -0
- package/packages/core/src/service.ts +380 -0
- package/packages/core/src/session.ts +480 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
- package/packages/core/src/static.ts +58 -0
- package/packages/core/src/testing.ts +233 -0
- package/packages/core/src/types.ts +98 -0
- package/packages/core/src/watcher.ts +37 -0
- package/packages/core/src/websocket.ts +408 -0
- package/packages/core/src/wsdl.ts +546 -0
- package/packages/core/templates/errors/302.twig +14 -0
- package/packages/core/templates/errors/401.twig +9 -0
- package/packages/core/templates/errors/403.twig +29 -0
- package/packages/core/templates/errors/404.twig +29 -0
- package/packages/core/templates/errors/500.twig +38 -0
- package/packages/core/templates/errors/502.twig +9 -0
- package/packages/core/templates/errors/503.twig +12 -0
- package/packages/core/templates/errors/base.twig +37 -0
- package/packages/frond/src/engine.ts +1475 -0
- package/packages/frond/src/index.ts +2 -0
- package/packages/orm/src/adapters/firebird.ts +455 -0
- package/packages/orm/src/adapters/mssql.ts +440 -0
- package/packages/orm/src/adapters/mysql.ts +355 -0
- package/packages/orm/src/adapters/postgres.ts +362 -0
- package/packages/orm/src/adapters/sqlite.ts +270 -0
- package/packages/orm/src/autoCrud.ts +231 -0
- package/packages/orm/src/baseModel.ts +536 -0
- package/packages/orm/src/database.ts +321 -0
- package/packages/orm/src/fakeData.ts +118 -0
- package/packages/orm/src/index.ts +49 -0
- package/packages/orm/src/migration.ts +392 -0
- package/packages/orm/src/model.ts +56 -0
- package/packages/orm/src/query.ts +113 -0
- package/packages/orm/src/seeder.ts +120 -0
- package/packages/orm/src/sqlTranslation.ts +272 -0
- package/packages/orm/src/types.ts +110 -0
- package/packages/orm/src/validation.ts +93 -0
- package/packages/swagger/src/generator.ts +189 -0
- package/packages/swagger/src/index.ts +2 -0
- package/packages/swagger/src/ui.ts +48 -0
- package/skills/tina4-developer.skill +0 -0
- package/skills/tina4-js.skill +0 -0
- package/skills/tina4-maintainer.skill +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 Firebird Adapter — uses the `node-firebird` package (optional peer dependency).
|
|
3
|
+
*
|
|
4
|
+
* Install: npm install node-firebird
|
|
5
|
+
* URL format: firebird://user:pass@host:port/path/to/database.fdb
|
|
6
|
+
*/
|
|
7
|
+
import type { DatabaseAdapter, DatabaseResult, ColumnInfo, FieldDefinition } from "../types.js";
|
|
8
|
+
import { SQLTranslator } from "../sqlTranslation.js";
|
|
9
|
+
|
|
10
|
+
let firebird: any = null;
|
|
11
|
+
|
|
12
|
+
function requireFirebird(): any {
|
|
13
|
+
if (firebird) return firebird;
|
|
14
|
+
try {
|
|
15
|
+
const { createRequire } = require("node:module") as typeof import("node:module");
|
|
16
|
+
const req = createRequire(import.meta.url);
|
|
17
|
+
firebird = req("node-firebird");
|
|
18
|
+
return firebird;
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'Firebird adapter requires the "node-firebird" package. Install it with: npm install node-firebird',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FirebirdConfig {
|
|
27
|
+
host?: string;
|
|
28
|
+
port?: number;
|
|
29
|
+
user?: string;
|
|
30
|
+
password?: string;
|
|
31
|
+
database?: string;
|
|
32
|
+
role?: string;
|
|
33
|
+
pageSize?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class FirebirdAdapter implements DatabaseAdapter {
|
|
37
|
+
private db: any = null;
|
|
38
|
+
private transaction: any = null;
|
|
39
|
+
private _lastInsertId: number | bigint | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(private config: FirebirdConfig | string) {}
|
|
42
|
+
|
|
43
|
+
/** Connect to Firebird. Must be called before using the adapter. */
|
|
44
|
+
async connect(): Promise<void> {
|
|
45
|
+
const fb = requireFirebird();
|
|
46
|
+
|
|
47
|
+
let fbConfig: any;
|
|
48
|
+
|
|
49
|
+
if (typeof this.config === "string") {
|
|
50
|
+
const parsed = this.parseUrl(this.config);
|
|
51
|
+
fbConfig = {
|
|
52
|
+
host: parsed.host ?? "localhost",
|
|
53
|
+
port: parsed.port ?? 3050,
|
|
54
|
+
database: parsed.database,
|
|
55
|
+
user: parsed.user ?? "SYSDBA",
|
|
56
|
+
password: parsed.password ?? "masterkey",
|
|
57
|
+
role: undefined,
|
|
58
|
+
pageSize: 4096,
|
|
59
|
+
};
|
|
60
|
+
} else {
|
|
61
|
+
fbConfig = {
|
|
62
|
+
host: this.config.host ?? "localhost",
|
|
63
|
+
port: this.config.port ?? 3050,
|
|
64
|
+
database: this.config.database,
|
|
65
|
+
user: this.config.user ?? "SYSDBA",
|
|
66
|
+
password: this.config.password ?? "masterkey",
|
|
67
|
+
role: this.config.role,
|
|
68
|
+
pageSize: this.config.pageSize ?? 4096,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
await new Promise<void>((resolve, reject) => {
|
|
73
|
+
fb.attach(fbConfig, (err: Error | null, db: any) => {
|
|
74
|
+
if (err) reject(err);
|
|
75
|
+
else {
|
|
76
|
+
this.db = db;
|
|
77
|
+
resolve();
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private parseUrl(url: string): { host?: string; port?: number; user?: string; password?: string; database?: string } {
|
|
84
|
+
// firebird://user:pass@host:port/path/to/db.fdb
|
|
85
|
+
const match = url.match(/firebird:\/\/(?:([^:]+):([^@]+)@)?([^:/]+)(?::(\d+))?\/(.*)/);
|
|
86
|
+
if (match) {
|
|
87
|
+
return {
|
|
88
|
+
user: match[1],
|
|
89
|
+
password: match[2],
|
|
90
|
+
host: match[3],
|
|
91
|
+
port: match[4] ? parseInt(match[4], 10) : undefined,
|
|
92
|
+
database: "/" + match[5],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
// Bare path
|
|
96
|
+
const barePath = url.replace(/^firebird:\/\//, "");
|
|
97
|
+
return { database: barePath };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private ensureConnected(): void {
|
|
101
|
+
if (!this.db) {
|
|
102
|
+
throw new Error("Firebird adapter not connected. Call connect() first.");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Translate SQL for Firebird dialect. */
|
|
107
|
+
translateSql(sql: string): string {
|
|
108
|
+
let translated = SQLTranslator.limitToRows(sql);
|
|
109
|
+
translated = SQLTranslator.booleanToInt(translated);
|
|
110
|
+
translated = SQLTranslator.ilikeToLike(translated);
|
|
111
|
+
return translated;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private queryPromise(sql: string, params?: unknown[]): Promise<any[]> {
|
|
115
|
+
return new Promise((resolve, reject) => {
|
|
116
|
+
const translated = this.translateSql(sql);
|
|
117
|
+
this.db.query(translated, params ?? [], (err: Error | null, result: any[]) => {
|
|
118
|
+
if (err) reject(err);
|
|
119
|
+
else resolve(result ?? []);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private executePromise(sql: string, params?: unknown[]): Promise<void> {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const translated = this.translateSql(sql);
|
|
127
|
+
this.db.execute(translated, params ?? [], (err: Error | null) => {
|
|
128
|
+
if (err) reject(err);
|
|
129
|
+
else resolve();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
execute(sql: string, params?: unknown[]): unknown {
|
|
135
|
+
throw new Error("Use executeAsync() for Firebird — async adapter requires async methods.");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
executeMany(sql: string, paramsList: unknown[][]): { totalAffected: number; lastInsertId?: number | bigint } {
|
|
139
|
+
throw new Error("Use executeManyAsync() for Firebird — async adapter requires async methods.");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
|
|
143
|
+
let totalAffected = 0;
|
|
144
|
+
for (const params of paramsList) {
|
|
145
|
+
await this.executeAsync(sql, params);
|
|
146
|
+
totalAffected++;
|
|
147
|
+
}
|
|
148
|
+
return { totalAffected };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
|
|
152
|
+
this.ensureConnected();
|
|
153
|
+
await this.executePromise(sql, params);
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
|
|
158
|
+
throw new Error("Use queryAsync() for Firebird.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
162
|
+
this.ensureConnected();
|
|
163
|
+
const rows = await this.queryPromise(sql, params);
|
|
164
|
+
return rows as T[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
|
|
168
|
+
throw new Error("Use fetchAsync() for Firebird.");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
|
|
172
|
+
let effectiveSql = sql;
|
|
173
|
+
if (limit !== undefined) {
|
|
174
|
+
const offset = skip ?? 0;
|
|
175
|
+
const start = offset + 1;
|
|
176
|
+
const end = offset + limit;
|
|
177
|
+
// Firebird uses ROWS X TO Y (or FIRST/SKIP)
|
|
178
|
+
effectiveSql += ` ROWS ${start} TO ${end}`;
|
|
179
|
+
}
|
|
180
|
+
return this.queryAsync<T>(effectiveSql, params);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
|
|
184
|
+
throw new Error("Use fetchOneAsync() for Firebird.");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
188
|
+
const rows = await this.fetchAsync<T>(sql, params, 1, 0);
|
|
189
|
+
return rows[0] ?? null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
insert(table: string, data: Record<string, unknown>): DatabaseResult {
|
|
193
|
+
throw new Error("Use insertAsync() for Firebird.");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
|
|
197
|
+
this.ensureConnected();
|
|
198
|
+
const keys = Object.keys(data);
|
|
199
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
200
|
+
const sql = `INSERT INTO "${table}" ("${keys.join('", "')}") VALUES (${placeholders})`;
|
|
201
|
+
const values = Object.values(data);
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await this.executePromise(sql, values);
|
|
205
|
+
// Firebird doesn't have a generic last_insert_id — return success without id
|
|
206
|
+
return {
|
|
207
|
+
success: true,
|
|
208
|
+
rowsAffected: 1,
|
|
209
|
+
};
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): DatabaseResult {
|
|
216
|
+
throw new Error("Use updateAsync() for Firebird.");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
220
|
+
this.ensureConnected();
|
|
221
|
+
const setClauses = Object.keys(data).map((k) => `"${k}" = ?`).join(", ");
|
|
222
|
+
const whereClauses = Object.keys(filter).map((k) => `"${k}" = ?`).join(" AND ");
|
|
223
|
+
const sql = `UPDATE "${table}" SET ${setClauses} WHERE ${whereClauses}`;
|
|
224
|
+
const values = [...Object.values(data), ...Object.values(filter)];
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
await this.executePromise(sql, values);
|
|
228
|
+
return { success: true, rowsAffected: 1 };
|
|
229
|
+
} catch (e) {
|
|
230
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
delete(table: string, filter: Record<string, unknown>): DatabaseResult {
|
|
235
|
+
throw new Error("Use deleteAsync() for Firebird.");
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async deleteAsync(table: string, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
239
|
+
this.ensureConnected();
|
|
240
|
+
const whereClauses = Object.keys(filter).map((k) => `"${k}" = ?`).join(" AND ");
|
|
241
|
+
const sql = `DELETE FROM "${table}" WHERE ${whereClauses}`;
|
|
242
|
+
const values = Object.values(filter);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
await this.executePromise(sql, values);
|
|
246
|
+
return { success: true, rowsAffected: 1 };
|
|
247
|
+
} catch (e) {
|
|
248
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
startTransaction(): void {
|
|
253
|
+
throw new Error("Use startTransactionAsync() for Firebird.");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async startTransactionAsync(): Promise<void> {
|
|
257
|
+
this.ensureConnected();
|
|
258
|
+
await new Promise<void>((resolve, reject) => {
|
|
259
|
+
this.db.transaction(0 /* ISOLATION_READ_COMMITTED */, (err: Error | null, transaction: any) => {
|
|
260
|
+
if (err) reject(err);
|
|
261
|
+
else {
|
|
262
|
+
this.transaction = transaction;
|
|
263
|
+
resolve();
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
commit(): void {
|
|
270
|
+
throw new Error("Use commitAsync() for Firebird.");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async commitAsync(): Promise<void> {
|
|
274
|
+
if (!this.transaction) throw new Error("No active transaction to commit.");
|
|
275
|
+
await new Promise<void>((resolve, reject) => {
|
|
276
|
+
this.transaction.commit((err: Error | null) => {
|
|
277
|
+
if (err) reject(err);
|
|
278
|
+
else {
|
|
279
|
+
this.transaction = null;
|
|
280
|
+
resolve();
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
rollback(): void {
|
|
287
|
+
throw new Error("Use rollbackAsync() for Firebird.");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async rollbackAsync(): Promise<void> {
|
|
291
|
+
if (!this.transaction) throw new Error("No active transaction to rollback.");
|
|
292
|
+
await new Promise<void>((resolve, reject) => {
|
|
293
|
+
this.transaction.rollback((err: Error | null) => {
|
|
294
|
+
if (err) reject(err);
|
|
295
|
+
else {
|
|
296
|
+
this.transaction = null;
|
|
297
|
+
resolve();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
tables(): string[] {
|
|
304
|
+
throw new Error("Use tablesAsync() for Firebird.");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async tablesAsync(): Promise<string[]> {
|
|
308
|
+
const rows = await this.queryAsync<Record<string, string>>(
|
|
309
|
+
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$SYSTEM_FLAG = 0 AND RDB$VIEW_BLR IS NULL",
|
|
310
|
+
);
|
|
311
|
+
return rows.map((r) => {
|
|
312
|
+
const name = r["RDB$RELATION_NAME"] ?? r["rdb$relation_name"] ?? "";
|
|
313
|
+
return typeof name === "string" ? name.trim() : String(name).trim();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
columns(table: string): ColumnInfo[] {
|
|
318
|
+
throw new Error("Use columnsAsync() for Firebird.");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async columnsAsync(table: string): Promise<ColumnInfo[]> {
|
|
322
|
+
const rows = await this.queryAsync<Record<string, unknown>>(
|
|
323
|
+
`SELECT RF.RDB$FIELD_NAME, F.RDB$FIELD_TYPE, RF.RDB$NULL_FLAG, RF.RDB$DEFAULT_SOURCE
|
|
324
|
+
FROM RDB$RELATION_FIELDS RF
|
|
325
|
+
JOIN RDB$FIELDS F ON RF.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME
|
|
326
|
+
WHERE RF.RDB$RELATION_NAME = ?`,
|
|
327
|
+
[table.toUpperCase()],
|
|
328
|
+
);
|
|
329
|
+
return rows.map((r) => {
|
|
330
|
+
const name = (r["RDB$FIELD_NAME"] ?? r["rdb$field_name"] ?? "") as string;
|
|
331
|
+
return {
|
|
332
|
+
name: typeof name === "string" ? name.trim() : String(name).trim(),
|
|
333
|
+
type: firebirdFieldTypeToString(r["RDB$FIELD_TYPE"] ?? r["rdb$field_type"]),
|
|
334
|
+
nullable: (r["RDB$NULL_FLAG"] ?? r["rdb$null_flag"]) === null,
|
|
335
|
+
default: r["RDB$DEFAULT_SOURCE"] ?? r["rdb$default_source"],
|
|
336
|
+
primaryKey: false,
|
|
337
|
+
};
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
lastInsertId(): number | bigint | null {
|
|
342
|
+
// Firebird doesn't have a generic last_insert_id
|
|
343
|
+
return this._lastInsertId;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
close(): void {
|
|
347
|
+
if (this.db) {
|
|
348
|
+
this.db.detach();
|
|
349
|
+
this.db = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
tableExists(name: string): boolean {
|
|
354
|
+
throw new Error("Use tableExistsAsync() for Firebird.");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async tableExistsAsync(name: string): Promise<boolean> {
|
|
358
|
+
const rows = await this.queryAsync<Record<string, unknown>>(
|
|
359
|
+
"SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = ?",
|
|
360
|
+
[name.toUpperCase()],
|
|
361
|
+
);
|
|
362
|
+
return rows.length > 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
createTable(name: string, columns: Record<string, FieldDefinition>): void {
|
|
366
|
+
throw new Error("Use createTableAsync() for Firebird.");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async createTableAsync(name: string, columns: Record<string, FieldDefinition>): Promise<void> {
|
|
370
|
+
// Check if table exists first — Firebird doesn't support IF NOT EXISTS on CREATE TABLE
|
|
371
|
+
const exists = await this.tableExistsAsync(name);
|
|
372
|
+
if (exists) return;
|
|
373
|
+
|
|
374
|
+
const colDefs: string[] = [];
|
|
375
|
+
|
|
376
|
+
for (const [colName, def] of Object.entries(columns)) {
|
|
377
|
+
const sqlType = fieldTypeToFirebird(def);
|
|
378
|
+
const parts = [`"${colName}" ${sqlType}`];
|
|
379
|
+
|
|
380
|
+
if (def.primaryKey && !def.autoIncrement) parts.push("PRIMARY KEY");
|
|
381
|
+
if (def.required && !def.primaryKey) parts.push("NOT NULL");
|
|
382
|
+
if (def.default !== undefined && def.default !== "now") {
|
|
383
|
+
parts.push(`DEFAULT ${sqlDefault(def.default)}`);
|
|
384
|
+
}
|
|
385
|
+
if (def.default === "now") {
|
|
386
|
+
parts.push("DEFAULT CURRENT_TIMESTAMP");
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
colDefs.push(parts.join(" "));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const sql = `CREATE TABLE "${name}" (${colDefs.join(", ")})`;
|
|
393
|
+
await this.executeAsync(sql);
|
|
394
|
+
|
|
395
|
+
// Create sequences and triggers for auto-increment columns
|
|
396
|
+
for (const [colName, def] of Object.entries(columns)) {
|
|
397
|
+
if (def.autoIncrement) {
|
|
398
|
+
const seqName = `GEN_${name}_${colName}`.toUpperCase();
|
|
399
|
+
const trigName = `TRG_${name}_${colName}`.toUpperCase();
|
|
400
|
+
|
|
401
|
+
await this.executeAsync(`CREATE SEQUENCE "${seqName}"`);
|
|
402
|
+
await this.executeAsync(
|
|
403
|
+
`CREATE TRIGGER "${trigName}" FOR "${name}" ACTIVE BEFORE INSERT POSITION 0 AS BEGIN IF (NEW."${colName}" IS NULL) THEN NEW."${colName}" = NEXT VALUE FOR "${seqName}"; END`,
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Convert Firebird internal field type codes to strings. */
|
|
411
|
+
function firebirdFieldTypeToString(typeCode: unknown): string {
|
|
412
|
+
switch (typeCode) {
|
|
413
|
+
case 7: return "SMALLINT";
|
|
414
|
+
case 8: return "INTEGER";
|
|
415
|
+
case 10: return "FLOAT";
|
|
416
|
+
case 12: return "DATE";
|
|
417
|
+
case 13: return "TIME";
|
|
418
|
+
case 14: return "CHAR";
|
|
419
|
+
case 16: return "BIGINT";
|
|
420
|
+
case 27: return "DOUBLE PRECISION";
|
|
421
|
+
case 35: return "TIMESTAMP";
|
|
422
|
+
case 37: return "VARCHAR";
|
|
423
|
+
case 261: return "BLOB";
|
|
424
|
+
default: return String(typeCode);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function fieldTypeToFirebird(def: FieldDefinition): string {
|
|
429
|
+
if (def.primaryKey && def.autoIncrement) {
|
|
430
|
+
return "INTEGER PRIMARY KEY";
|
|
431
|
+
}
|
|
432
|
+
switch (def.type) {
|
|
433
|
+
case "integer":
|
|
434
|
+
return "INTEGER";
|
|
435
|
+
case "number":
|
|
436
|
+
case "numeric":
|
|
437
|
+
return "DOUBLE PRECISION";
|
|
438
|
+
case "boolean":
|
|
439
|
+
return "SMALLINT";
|
|
440
|
+
case "datetime":
|
|
441
|
+
return "TIMESTAMP";
|
|
442
|
+
case "text":
|
|
443
|
+
return "BLOB SUB_TYPE TEXT";
|
|
444
|
+
case "string":
|
|
445
|
+
return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
|
|
446
|
+
default:
|
|
447
|
+
return "VARCHAR(255)";
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function sqlDefault(value: unknown): string {
|
|
452
|
+
if (typeof value === "string") return `'${value}'`;
|
|
453
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
454
|
+
return String(value);
|
|
455
|
+
}
|