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,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tina4 MSSQL Adapter — uses the `tedious` package (optional peer dependency).
|
|
3
|
+
*
|
|
4
|
+
* Install: npm install tedious
|
|
5
|
+
* URL format: mssql://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 tedious: any = null;
|
|
11
|
+
|
|
12
|
+
function requireTedious(): any {
|
|
13
|
+
if (tedious) return tedious;
|
|
14
|
+
try {
|
|
15
|
+
const { createRequire } = require("node:module") as typeof import("node:module");
|
|
16
|
+
const req = createRequire(import.meta.url);
|
|
17
|
+
tedious = req("tedious");
|
|
18
|
+
return tedious;
|
|
19
|
+
} catch {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'MSSQL adapter requires the "tedious" package. Install it with: npm install tedious',
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MssqlConfig {
|
|
27
|
+
host?: string;
|
|
28
|
+
port?: number;
|
|
29
|
+
user?: string;
|
|
30
|
+
password?: string;
|
|
31
|
+
database?: string;
|
|
32
|
+
connectionString?: string;
|
|
33
|
+
options?: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class MssqlAdapter implements DatabaseAdapter {
|
|
37
|
+
private connection: any = null;
|
|
38
|
+
private _lastInsertId: number | bigint | null = null;
|
|
39
|
+
|
|
40
|
+
constructor(private config: MssqlConfig | string) {}
|
|
41
|
+
|
|
42
|
+
/** Connect to MSSQL. Must be called before using the adapter. */
|
|
43
|
+
async connect(): Promise<void> {
|
|
44
|
+
const tediousModule = requireTedious();
|
|
45
|
+
const Connection = tediousModule.Connection;
|
|
46
|
+
|
|
47
|
+
let tediousConfig: any;
|
|
48
|
+
|
|
49
|
+
if (typeof this.config === "string") {
|
|
50
|
+
const parsed = this.parseUrl(this.config);
|
|
51
|
+
tediousConfig = {
|
|
52
|
+
server: parsed.host,
|
|
53
|
+
authentication: {
|
|
54
|
+
type: "default",
|
|
55
|
+
options: {
|
|
56
|
+
userName: parsed.user,
|
|
57
|
+
password: parsed.password,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
options: {
|
|
61
|
+
database: parsed.database,
|
|
62
|
+
port: parsed.port ?? 1433,
|
|
63
|
+
trustServerCertificate: true,
|
|
64
|
+
encrypt: false,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
tediousConfig = {
|
|
69
|
+
server: this.config.host ?? "localhost",
|
|
70
|
+
authentication: {
|
|
71
|
+
type: "default",
|
|
72
|
+
options: {
|
|
73
|
+
userName: this.config.user,
|
|
74
|
+
password: this.config.password,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
options: {
|
|
78
|
+
database: this.config.database,
|
|
79
|
+
port: this.config.port ?? 1433,
|
|
80
|
+
trustServerCertificate: true,
|
|
81
|
+
encrypt: false,
|
|
82
|
+
...this.config.options,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await new Promise<void>((resolve, reject) => {
|
|
88
|
+
this.connection = new Connection(tediousConfig);
|
|
89
|
+
this.connection.on("connect", (err: Error | null) => {
|
|
90
|
+
if (err) reject(err);
|
|
91
|
+
else resolve();
|
|
92
|
+
});
|
|
93
|
+
this.connection.connect();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private parseUrl(url: string): { host: string; port?: number; user?: string; password?: string; database?: string } {
|
|
98
|
+
const match = url.match(/(?:mssql|sqlserver):\/\/(?:([^:]+):([^@]+)@)?([^:/]+)(?::(\d+))?\/(.+)/);
|
|
99
|
+
if (match) {
|
|
100
|
+
return {
|
|
101
|
+
user: match[1],
|
|
102
|
+
password: match[2],
|
|
103
|
+
host: match[3],
|
|
104
|
+
port: match[4] ? parseInt(match[4], 10) : undefined,
|
|
105
|
+
database: match[5],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { host: "localhost", database: url };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private ensureConnected(): void {
|
|
112
|
+
if (!this.connection) {
|
|
113
|
+
throw new Error("MSSQL adapter not connected. Call connect() first.");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Translate SQL for MSSQL dialect. */
|
|
118
|
+
translateSql(sql: string): string {
|
|
119
|
+
let translated = SQLTranslator.limitToTop(sql);
|
|
120
|
+
translated = SQLTranslator.concatPipesToFunc(translated);
|
|
121
|
+
translated = SQLTranslator.ilikeToLike(translated);
|
|
122
|
+
return translated;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private execSqlPromise(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[]; rowCount: number }> {
|
|
126
|
+
const tediousModule = requireTedious();
|
|
127
|
+
const Request = tediousModule.Request;
|
|
128
|
+
const TYPES = tediousModule.TYPES;
|
|
129
|
+
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const rows: Record<string, unknown>[] = [];
|
|
132
|
+
const request = new Request(sql, (err: Error | null, rowCount: number) => {
|
|
133
|
+
if (err) reject(err);
|
|
134
|
+
else resolve({ rows, rowCount });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Add parameters
|
|
138
|
+
if (params) {
|
|
139
|
+
params.forEach((p, i) => {
|
|
140
|
+
const paramName = `p${i}`;
|
|
141
|
+
let type = TYPES.NVarChar;
|
|
142
|
+
if (typeof p === "number") type = Number.isInteger(p) ? TYPES.Int : TYPES.Float;
|
|
143
|
+
else if (typeof p === "boolean") type = TYPES.Bit;
|
|
144
|
+
else if (p instanceof Date) type = TYPES.DateTime;
|
|
145
|
+
request.addParameter(paramName, type, p);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
request.on("row", (columns: any[]) => {
|
|
150
|
+
const row: Record<string, unknown> = {};
|
|
151
|
+
columns.forEach((col: any) => {
|
|
152
|
+
row[col.metadata.colName] = col.value;
|
|
153
|
+
});
|
|
154
|
+
rows.push(row);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.connection.execSql(request);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Convert ? placeholders to @p0, @p1, ... for tedious. */
|
|
162
|
+
private convertPlaceholders(sql: string): string {
|
|
163
|
+
let count = 0;
|
|
164
|
+
return sql.replace(/\?/g, () => {
|
|
165
|
+
return `@p${count++}`;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
execute(sql: string, params?: unknown[]): unknown {
|
|
170
|
+
throw new Error("Use executeAsync() for MSSQL — async adapter requires async methods.");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
executeMany(sql: string, paramsList: unknown[][]): { totalAffected: number; lastInsertId?: number | bigint } {
|
|
174
|
+
throw new Error("Use executeManyAsync() for MSSQL — async adapter requires async methods.");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async executeManyAsync(sql: string, paramsList: unknown[][]): Promise<{ totalAffected: number; lastInsertId?: number | bigint }> {
|
|
178
|
+
let totalAffected = 0;
|
|
179
|
+
for (const params of paramsList) {
|
|
180
|
+
await this.executeAsync(sql, params);
|
|
181
|
+
totalAffected++;
|
|
182
|
+
}
|
|
183
|
+
return { totalAffected };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async executeAsync(sql: string, params?: unknown[]): Promise<unknown> {
|
|
187
|
+
this.ensureConnected();
|
|
188
|
+
const translated = this.translateSql(sql);
|
|
189
|
+
const converted = this.convertPlaceholders(translated);
|
|
190
|
+
return this.execSqlPromise(converted, params);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[] {
|
|
194
|
+
throw new Error("Use queryAsync() for MSSQL.");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async queryAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]> {
|
|
198
|
+
this.ensureConnected();
|
|
199
|
+
const translated = this.translateSql(sql);
|
|
200
|
+
const converted = this.convertPlaceholders(translated);
|
|
201
|
+
const result = await this.execSqlPromise(converted, params);
|
|
202
|
+
return result.rows as T[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fetch<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): T[] {
|
|
206
|
+
throw new Error("Use fetchAsync() for MSSQL.");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async fetchAsync<T = Record<string, unknown>>(sql: string, params?: unknown[], limit?: number, skip?: number): Promise<T[]> {
|
|
210
|
+
let effectiveSql = sql;
|
|
211
|
+
if (limit !== undefined) {
|
|
212
|
+
if (skip !== undefined && skip > 0) {
|
|
213
|
+
// MSSQL uses OFFSET...FETCH for pagination (requires ORDER BY)
|
|
214
|
+
if (!/ORDER BY/i.test(effectiveSql)) {
|
|
215
|
+
effectiveSql += " ORDER BY (SELECT NULL)";
|
|
216
|
+
}
|
|
217
|
+
effectiveSql += ` OFFSET ${skip} ROWS FETCH NEXT ${limit} ROWS ONLY`;
|
|
218
|
+
} else {
|
|
219
|
+
// Use TOP for simple limit
|
|
220
|
+
effectiveSql = effectiveSql.replace(/^(SELECT)\b/i, `$1 TOP ${limit}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return this.queryAsync<T>(effectiveSql, params);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fetchOne<T = Record<string, unknown>>(sql: string, params?: unknown[]): T | null {
|
|
227
|
+
throw new Error("Use fetchOneAsync() for MSSQL.");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async fetchOneAsync<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T | null> {
|
|
231
|
+
const rows = await this.queryAsync<T>(sql, params);
|
|
232
|
+
return rows[0] ?? null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
insert(table: string, data: Record<string, unknown>): DatabaseResult {
|
|
236
|
+
throw new Error("Use insertAsync() for MSSQL.");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async insertAsync(table: string, data: Record<string, unknown>): Promise<DatabaseResult> {
|
|
240
|
+
this.ensureConnected();
|
|
241
|
+
const keys = Object.keys(data);
|
|
242
|
+
const placeholders = keys.map((_, i) => `@p${i}`).join(", ");
|
|
243
|
+
const sql = `INSERT INTO [${table}] ([${keys.join("], [")}]) VALUES (${placeholders}); SELECT SCOPE_IDENTITY() AS id`;
|
|
244
|
+
const values = Object.values(data);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const result = await this.execSqlPromise(sql, values);
|
|
248
|
+
const id = result.rows[0]?.id as number ?? null;
|
|
249
|
+
if (id !== null) this._lastInsertId = id;
|
|
250
|
+
return {
|
|
251
|
+
success: true,
|
|
252
|
+
rowsAffected: result.rowCount,
|
|
253
|
+
lastInsertId: id ?? undefined,
|
|
254
|
+
};
|
|
255
|
+
} catch (e) {
|
|
256
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
update(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): DatabaseResult {
|
|
261
|
+
throw new Error("Use updateAsync() for MSSQL.");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async updateAsync(table: string, data: Record<string, unknown>, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
265
|
+
this.ensureConnected();
|
|
266
|
+
const dataKeys = Object.keys(data);
|
|
267
|
+
const filterKeys = Object.keys(filter);
|
|
268
|
+
let paramIndex = 0;
|
|
269
|
+
|
|
270
|
+
const setClauses = dataKeys.map((k) => `[${k}] = @p${paramIndex++}`).join(", ");
|
|
271
|
+
const whereClauses = filterKeys.map((k) => `[${k}] = @p${paramIndex++}`).join(" AND ");
|
|
272
|
+
const sql = `UPDATE [${table}] SET ${setClauses} WHERE ${whereClauses}`;
|
|
273
|
+
const values = [...Object.values(data), ...Object.values(filter)];
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const result = await this.execSqlPromise(sql, values);
|
|
277
|
+
return { success: true, rowsAffected: result.rowCount };
|
|
278
|
+
} catch (e) {
|
|
279
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
delete(table: string, filter: Record<string, unknown>): DatabaseResult {
|
|
284
|
+
throw new Error("Use deleteAsync() for MSSQL.");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async deleteAsync(table: string, filter: Record<string, unknown>): Promise<DatabaseResult> {
|
|
288
|
+
this.ensureConnected();
|
|
289
|
+
const filterKeys = Object.keys(filter);
|
|
290
|
+
let paramIndex = 0;
|
|
291
|
+
const whereClauses = filterKeys.map((k) => `[${k}] = @p${paramIndex++}`).join(" AND ");
|
|
292
|
+
const sql = `DELETE FROM [${table}] WHERE ${whereClauses}`;
|
|
293
|
+
const values = Object.values(filter);
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const result = await this.execSqlPromise(sql, values);
|
|
297
|
+
return { success: true, rowsAffected: result.rowCount };
|
|
298
|
+
} catch (e) {
|
|
299
|
+
return { success: false, rowsAffected: 0, error: (e as Error).message };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
startTransaction(): void {
|
|
304
|
+
throw new Error("Use startTransactionAsync() for MSSQL.");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async startTransactionAsync(): Promise<void> {
|
|
308
|
+
await this.executeAsync("BEGIN TRANSACTION");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
commit(): void {
|
|
312
|
+
throw new Error("Use commitAsync() for MSSQL.");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async commitAsync(): Promise<void> {
|
|
316
|
+
await this.executeAsync("COMMIT");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
rollback(): void {
|
|
320
|
+
throw new Error("Use rollbackAsync() for MSSQL.");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async rollbackAsync(): Promise<void> {
|
|
324
|
+
await this.executeAsync("ROLLBACK");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
tables(): string[] {
|
|
328
|
+
throw new Error("Use tablesAsync() for MSSQL.");
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async tablesAsync(): Promise<string[]> {
|
|
332
|
+
const rows = await this.queryAsync<{ TABLE_NAME: string }>(
|
|
333
|
+
"SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'",
|
|
334
|
+
);
|
|
335
|
+
return rows.map((r) => r.TABLE_NAME);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
columns(table: string): ColumnInfo[] {
|
|
339
|
+
throw new Error("Use columnsAsync() for MSSQL.");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async columnsAsync(table: string): Promise<ColumnInfo[]> {
|
|
343
|
+
const rows = await this.queryAsync<{
|
|
344
|
+
COLUMN_NAME: string;
|
|
345
|
+
DATA_TYPE: string;
|
|
346
|
+
IS_NULLABLE: string;
|
|
347
|
+
COLUMN_DEFAULT: string | null;
|
|
348
|
+
}>(
|
|
349
|
+
"SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = ?",
|
|
350
|
+
[table],
|
|
351
|
+
);
|
|
352
|
+
return rows.map((r) => ({
|
|
353
|
+
name: r.COLUMN_NAME,
|
|
354
|
+
type: r.DATA_TYPE,
|
|
355
|
+
nullable: r.IS_NULLABLE === "YES",
|
|
356
|
+
default: r.COLUMN_DEFAULT,
|
|
357
|
+
primaryKey: false,
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
lastInsertId(): number | bigint | null {
|
|
362
|
+
return this._lastInsertId;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
close(): void {
|
|
366
|
+
if (this.connection) {
|
|
367
|
+
this.connection.close();
|
|
368
|
+
this.connection = null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
tableExists(name: string): boolean {
|
|
373
|
+
throw new Error("Use tableExistsAsync() for MSSQL.");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async tableExistsAsync(name: string): Promise<boolean> {
|
|
377
|
+
const rows = await this.queryAsync<{ cnt: number }>(
|
|
378
|
+
"SELECT COUNT(*) AS cnt FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = ?",
|
|
379
|
+
[name],
|
|
380
|
+
);
|
|
381
|
+
return (rows[0]?.cnt ?? 0) > 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
createTable(name: string, columns: Record<string, FieldDefinition>): void {
|
|
385
|
+
throw new Error("Use createTableAsync() for MSSQL.");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async createTableAsync(name: string, columns: Record<string, FieldDefinition>): Promise<void> {
|
|
389
|
+
const colDefs: string[] = [];
|
|
390
|
+
|
|
391
|
+
for (const [colName, def] of Object.entries(columns)) {
|
|
392
|
+
const sqlType = fieldTypeToMssql(def);
|
|
393
|
+
const parts = [`[${colName}] ${sqlType}`];
|
|
394
|
+
|
|
395
|
+
if (def.primaryKey && !def.autoIncrement) parts.push("PRIMARY KEY");
|
|
396
|
+
if (def.required && !def.primaryKey) parts.push("NOT NULL");
|
|
397
|
+
if (def.default !== undefined && def.default !== "now") {
|
|
398
|
+
parts.push(`DEFAULT ${sqlDefault(def.default)}`);
|
|
399
|
+
}
|
|
400
|
+
if (def.default === "now") {
|
|
401
|
+
parts.push("DEFAULT GETDATE()");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
colDefs.push(parts.join(" "));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// MSSQL doesn't have IF NOT EXISTS — use conditional DDL
|
|
408
|
+
const sql = `IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '${name}') CREATE TABLE [${name}] (${colDefs.join(", ")})`;
|
|
409
|
+
await this.executeAsync(sql);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function fieldTypeToMssql(def: FieldDefinition): string {
|
|
414
|
+
if (def.primaryKey && def.autoIncrement) {
|
|
415
|
+
return "INT IDENTITY(1,1) PRIMARY KEY";
|
|
416
|
+
}
|
|
417
|
+
switch (def.type) {
|
|
418
|
+
case "integer":
|
|
419
|
+
return "INT";
|
|
420
|
+
case "number":
|
|
421
|
+
case "numeric":
|
|
422
|
+
return "FLOAT";
|
|
423
|
+
case "boolean":
|
|
424
|
+
return "BIT";
|
|
425
|
+
case "datetime":
|
|
426
|
+
return "DATETIME";
|
|
427
|
+
case "text":
|
|
428
|
+
return "NTEXT";
|
|
429
|
+
case "string":
|
|
430
|
+
return def.maxLength ? `NVARCHAR(${def.maxLength})` : "NVARCHAR(255)";
|
|
431
|
+
default:
|
|
432
|
+
return "NVARCHAR(MAX)";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function sqlDefault(value: unknown): string {
|
|
437
|
+
if (typeof value === "string") return `'${value}'`;
|
|
438
|
+
if (typeof value === "boolean") return value ? "1" : "0";
|
|
439
|
+
return String(value);
|
|
440
|
+
}
|