@objectstack/driver-sql 3.2.9

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/dist/index.mjs ADDED
@@ -0,0 +1,1007 @@
1
+ // src/sql-driver.ts
2
+ import knex from "knex";
3
+ import { nanoid } from "nanoid";
4
+ var DEFAULT_ID_LENGTH = 16;
5
+ var SqlDriver = class {
6
+ constructor(config) {
7
+ // DriverInterface metadata
8
+ this.name = "com.objectstack.driver.sql";
9
+ this.version = "1.0.0";
10
+ this.supports = {
11
+ transactions: true,
12
+ joins: true,
13
+ fullTextSearch: false,
14
+ jsonFields: true,
15
+ arrayFields: true,
16
+ queryFilters: true,
17
+ queryAggregations: true,
18
+ querySorting: true,
19
+ queryPagination: true,
20
+ queryWindowFunctions: true,
21
+ querySubqueries: true
22
+ };
23
+ this.jsonFields = {};
24
+ this.booleanFields = {};
25
+ this.tablesWithTimestamps = /* @__PURE__ */ new Set();
26
+ this.config = config;
27
+ this.knex = knex(config);
28
+ }
29
+ /** Whether the underlying database is a SQLite variant (sqlite3 or better-sqlite3). */
30
+ get isSqlite() {
31
+ const c = this.config.client;
32
+ return c === "sqlite3" || c === "better-sqlite3";
33
+ }
34
+ /** Whether the underlying database is PostgreSQL. */
35
+ get isPostgres() {
36
+ const c = this.config.client;
37
+ return c === "pg" || c === "postgresql";
38
+ }
39
+ /** Whether the underlying database is MySQL. */
40
+ get isMysql() {
41
+ const c = this.config.client;
42
+ return c === "mysql" || c === "mysql2";
43
+ }
44
+ // ===================================
45
+ // Lifecycle
46
+ // ===================================
47
+ async connect() {
48
+ return Promise.resolve();
49
+ }
50
+ async checkHealth() {
51
+ try {
52
+ await this.knex.raw("SELECT 1");
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+ async disconnect() {
59
+ await this.knex.destroy();
60
+ }
61
+ // ===================================
62
+ // CRUD — DriverInterface core
63
+ // ===================================
64
+ async find(object, query, options) {
65
+ const builder = this.getBuilder(object, options);
66
+ if (query.fields) {
67
+ builder.select(query.fields.map((f) => this.mapSortField(f)));
68
+ } else {
69
+ builder.select("*");
70
+ }
71
+ const filterCondition = query.where || query.filters;
72
+ if (filterCondition) {
73
+ this.applyFilters(builder, filterCondition);
74
+ }
75
+ const sortArray = query.orderBy || query.sort;
76
+ if (sortArray && Array.isArray(sortArray)) {
77
+ for (const item of sortArray) {
78
+ const field = item.field || item[0];
79
+ const dir = item.order || item[1] || "asc";
80
+ if (field) {
81
+ builder.orderBy(this.mapSortField(field), dir);
82
+ }
83
+ }
84
+ }
85
+ const offsetValue = query.offset ?? query.skip;
86
+ const limitValue = query.limit ?? query.top;
87
+ if (offsetValue !== void 0) builder.offset(offsetValue);
88
+ if (limitValue !== void 0) builder.limit(limitValue);
89
+ let results;
90
+ try {
91
+ results = await builder;
92
+ } catch (error) {
93
+ if (error.message && (error.message.includes("no such column") || error.message.includes("column") && error.message.includes("does not exist"))) {
94
+ return [];
95
+ }
96
+ throw error;
97
+ }
98
+ if (!Array.isArray(results)) {
99
+ return [];
100
+ }
101
+ if (this.isSqlite) {
102
+ for (const row of results) {
103
+ this.formatOutput(object, row);
104
+ }
105
+ }
106
+ return results;
107
+ }
108
+ async findOne(object, query, options) {
109
+ if (typeof query === "string" || typeof query === "number") {
110
+ const res = await this.getBuilder(object, options).where("id", query).first();
111
+ return this.formatOutput(object, res) || null;
112
+ }
113
+ if (query && typeof query === "object") {
114
+ const results = await this.find(object, { ...query, limit: 1 }, options);
115
+ return results[0] || null;
116
+ }
117
+ return null;
118
+ }
119
+ async create(object, data, options) {
120
+ const { _id, ...rest } = data;
121
+ const toInsert = { ...rest };
122
+ if (_id !== void 0 && toInsert.id === void 0) {
123
+ toInsert.id = _id;
124
+ } else if (toInsert.id === void 0) {
125
+ toInsert.id = nanoid(DEFAULT_ID_LENGTH);
126
+ }
127
+ const builder = this.getBuilder(object, options);
128
+ const formatted = this.formatInput(object, toInsert);
129
+ const result = await builder.insert(formatted).returning("*");
130
+ return this.formatOutput(object, result[0]);
131
+ }
132
+ async update(object, id, data, options) {
133
+ const builder = this.getBuilder(object, options);
134
+ const formatted = this.formatInput(object, data);
135
+ if (this.tablesWithTimestamps.has(object)) {
136
+ if (this.isSqlite) {
137
+ const now = /* @__PURE__ */ new Date();
138
+ formatted.updated_at = now.toISOString().replace("T", " ").replace("Z", "");
139
+ } else {
140
+ formatted.updated_at = this.knex.fn.now();
141
+ }
142
+ }
143
+ await builder.where("id", id).update(formatted);
144
+ const updated = await this.getBuilder(object, options).where("id", id).first();
145
+ return this.formatOutput(object, updated) || null;
146
+ }
147
+ async delete(object, id, options) {
148
+ const builder = this.getBuilder(object, options);
149
+ return await builder.where("id", id).delete();
150
+ }
151
+ // ===================================
152
+ // Optional — bulk & batch
153
+ // ===================================
154
+ async bulkCreate(object, data, options) {
155
+ const builder = this.getBuilder(object, options);
156
+ return await builder.insert(data).returning("*");
157
+ }
158
+ async updateMany(object, query, data, options) {
159
+ const builder = this.getBuilder(object, options);
160
+ const filters = query.where || query.filters || query;
161
+ if (filters) this.applyFilters(builder, filters);
162
+ const count = await builder.update(data);
163
+ return { modifiedCount: count || 0 };
164
+ }
165
+ async deleteMany(object, query, options) {
166
+ const builder = this.getBuilder(object, options);
167
+ const filters = query.where || query.filters || query;
168
+ if (filters) this.applyFilters(builder, filters);
169
+ const count = await builder.delete();
170
+ return { deletedCount: count || 0 };
171
+ }
172
+ async count(object, query, options) {
173
+ const builder = this.getBuilder(object, options);
174
+ let actualFilters = query;
175
+ if (query && (query.where || query.filters)) {
176
+ actualFilters = query.where || query.filters;
177
+ }
178
+ if (actualFilters) {
179
+ this.applyFilters(builder, actualFilters);
180
+ }
181
+ const result = await builder.count("* as count");
182
+ if (result && result.length > 0) {
183
+ const row = result[0];
184
+ return Number(row.count || row["count(*)"]);
185
+ }
186
+ return 0;
187
+ }
188
+ // ===================================
189
+ // Raw Execution
190
+ // ===================================
191
+ async execute(command, params, options) {
192
+ if (typeof command !== "string") {
193
+ return command;
194
+ }
195
+ const builder = options?.transaction ? this.knex.raw(command, params || []).transacting(options.transaction) : this.knex.raw(command, params || []);
196
+ return await builder;
197
+ }
198
+ // ===================================
199
+ // Transactions
200
+ // ===================================
201
+ async beginTransaction() {
202
+ return await this.knex.transaction();
203
+ }
204
+ async commitTransaction(trx) {
205
+ await trx.commit();
206
+ }
207
+ async rollbackTransaction(trx) {
208
+ await trx.rollback();
209
+ }
210
+ // ===================================
211
+ // Aggregation
212
+ // ===================================
213
+ async aggregate(object, query, options) {
214
+ const builder = this.getBuilder(object, options);
215
+ if (query.where) {
216
+ this.applyFilters(builder, query.where);
217
+ }
218
+ if (query.groupBy) {
219
+ builder.groupBy(query.groupBy);
220
+ for (const field of query.groupBy) {
221
+ builder.select(field);
222
+ }
223
+ }
224
+ const aggregates = query.aggregations || query.aggregate;
225
+ if (aggregates) {
226
+ for (const agg of aggregates) {
227
+ const funcName = agg.function || agg.func;
228
+ const rawFunc = this.mapAggregateFunc(funcName);
229
+ if (agg.alias) {
230
+ builder.select(this.knex.raw(`${rawFunc}(??) as ??`, [agg.field, agg.alias]));
231
+ } else {
232
+ builder.select(this.knex.raw(`${rawFunc}(??)`, [agg.field]));
233
+ }
234
+ }
235
+ }
236
+ return await builder;
237
+ }
238
+ // ===================================
239
+ // Distinct
240
+ // ===================================
241
+ async distinct(object, field, filters, options) {
242
+ const builder = this.getBuilder(object, options);
243
+ if (filters) {
244
+ this.applyFilters(builder, filters);
245
+ }
246
+ builder.distinct(field);
247
+ const results = await builder;
248
+ return results.map((row) => row[field]);
249
+ }
250
+ // ===================================
251
+ // Window Functions
252
+ // ===================================
253
+ async findWithWindowFunctions(object, query, options) {
254
+ const builder = this.getBuilder(object, options);
255
+ builder.select("*");
256
+ if (query.where) {
257
+ this.applyFilters(builder, query.where);
258
+ }
259
+ if (query.windowFunctions && Array.isArray(query.windowFunctions)) {
260
+ for (const wf of query.windowFunctions) {
261
+ const windowFunc = this.buildWindowFunction(wf);
262
+ builder.select(this.knex.raw(`${windowFunc} as ??`, [wf.alias]));
263
+ }
264
+ }
265
+ if (query.orderBy && Array.isArray(query.orderBy)) {
266
+ for (const sort of query.orderBy) {
267
+ builder.orderBy(this.mapSortField(sort.field), sort.order || "asc");
268
+ }
269
+ }
270
+ if (query.limit) builder.limit(query.limit);
271
+ if (query.offset) builder.offset(query.offset);
272
+ return await builder;
273
+ }
274
+ // ===================================
275
+ // Query Plan Analysis
276
+ // ===================================
277
+ async analyzeQuery(object, query, options) {
278
+ const builder = this.getBuilder(object, options);
279
+ if (query.fields) {
280
+ builder.select(query.fields);
281
+ } else {
282
+ builder.select("*");
283
+ }
284
+ if (query.where) {
285
+ this.applyFilters(builder, query.where);
286
+ }
287
+ if (query.orderBy && Array.isArray(query.orderBy)) {
288
+ for (const sort of query.orderBy) {
289
+ builder.orderBy(this.mapSortField(sort.field), sort.order || "asc");
290
+ }
291
+ }
292
+ if (query.limit) builder.limit(query.limit);
293
+ if (query.offset) builder.offset(query.offset);
294
+ const sql = builder.toSQL();
295
+ const client = this.config.client;
296
+ let explainResults;
297
+ try {
298
+ if (this.isPostgres) {
299
+ explainResults = await this.knex.raw(`EXPLAIN (FORMAT JSON, ANALYZE) ${sql.sql}`, sql.bindings);
300
+ } else if (this.isMysql) {
301
+ explainResults = await this.knex.raw(`EXPLAIN FORMAT=JSON ${sql.sql}`, sql.bindings);
302
+ } else if (this.isSqlite) {
303
+ explainResults = await this.knex.raw(`EXPLAIN QUERY PLAN ${sql.sql}`, sql.bindings);
304
+ } else {
305
+ return {
306
+ sql: sql.sql,
307
+ bindings: sql.bindings,
308
+ client,
309
+ note: "EXPLAIN not supported for this database client"
310
+ };
311
+ }
312
+ return { sql: sql.sql, bindings: sql.bindings, client, plan: explainResults };
313
+ } catch (error) {
314
+ return {
315
+ sql: sql.sql,
316
+ bindings: sql.bindings,
317
+ client,
318
+ error: error.message,
319
+ note: "Failed to execute EXPLAIN."
320
+ };
321
+ }
322
+ }
323
+ // ===================================
324
+ // Schema Sync (syncSchema / init)
325
+ // ===================================
326
+ async syncSchema(object, schema, _options) {
327
+ const objectDef = schema;
328
+ await this.initObjects([objectDef]);
329
+ }
330
+ /**
331
+ * Batch-initialise tables from an array of object definitions.
332
+ */
333
+ async initObjects(objects) {
334
+ await this.ensureDatabaseExists();
335
+ for (const obj of objects) {
336
+ const tableName = obj.name;
337
+ const jsonCols = [];
338
+ const booleanCols = [];
339
+ if (obj.fields) {
340
+ for (const [name, field] of Object.entries(obj.fields)) {
341
+ const type = field.type || "string";
342
+ if (this.isJsonField(type, field)) {
343
+ jsonCols.push(name);
344
+ }
345
+ if (type === "boolean") {
346
+ booleanCols.push(name);
347
+ }
348
+ }
349
+ }
350
+ this.jsonFields[tableName] = jsonCols;
351
+ this.booleanFields[tableName] = booleanCols;
352
+ let exists = await this.knex.schema.hasTable(tableName);
353
+ if (exists) {
354
+ const columnInfo = await this.knex(tableName).columnInfo();
355
+ const existingColumns = Object.keys(columnInfo);
356
+ if (existingColumns.includes("_id") && !existingColumns.includes("id")) {
357
+ await this.knex.schema.dropTable(tableName);
358
+ exists = false;
359
+ }
360
+ }
361
+ if (!exists) {
362
+ await this.knex.schema.createTable(tableName, (table) => {
363
+ table.string("id").primary();
364
+ table.timestamp("created_at").defaultTo(this.knex.fn.now());
365
+ table.timestamp("updated_at").defaultTo(this.knex.fn.now());
366
+ if (obj.fields) {
367
+ for (const [name, field] of Object.entries(obj.fields)) {
368
+ this.createColumn(table, name, field);
369
+ }
370
+ }
371
+ });
372
+ this.tablesWithTimestamps.add(tableName);
373
+ } else {
374
+ const columnInfo = await this.knex(tableName).columnInfo();
375
+ const existingColumns = Object.keys(columnInfo);
376
+ if (existingColumns.includes("updated_at")) {
377
+ this.tablesWithTimestamps.add(tableName);
378
+ }
379
+ await this.knex.schema.alterTable(tableName, (table) => {
380
+ if (obj.fields) {
381
+ for (const [name, field] of Object.entries(obj.fields)) {
382
+ if (!existingColumns.includes(name)) {
383
+ this.createColumn(table, name, field);
384
+ }
385
+ }
386
+ }
387
+ });
388
+ }
389
+ }
390
+ }
391
+ // ===================================
392
+ // Schema Introspection
393
+ // ===================================
394
+ async introspectSchema() {
395
+ const tables = {};
396
+ let tableNames = [];
397
+ if (this.isPostgres) {
398
+ const result = await this.knex.raw(`
399
+ SELECT table_name
400
+ FROM information_schema.tables
401
+ WHERE table_schema = 'public'
402
+ AND table_type = 'BASE TABLE'
403
+ `);
404
+ tableNames = result.rows.map((row) => row.table_name);
405
+ } else if (this.isMysql) {
406
+ const result = await this.knex.raw(`
407
+ SELECT table_name
408
+ FROM information_schema.tables
409
+ WHERE table_schema = DATABASE()
410
+ AND table_type = 'BASE TABLE'
411
+ `);
412
+ tableNames = result[0].map((row) => row.TABLE_NAME);
413
+ } else if (this.isSqlite) {
414
+ const result = await this.knex.raw(`
415
+ SELECT name as table_name
416
+ FROM sqlite_master
417
+ WHERE type='table'
418
+ AND name NOT LIKE 'sqlite_%'
419
+ `);
420
+ tableNames = result.map((row) => row.table_name);
421
+ }
422
+ for (const tableName of tableNames) {
423
+ const columns = await this.introspectColumns(tableName);
424
+ const foreignKeys = await this.introspectForeignKeys(tableName);
425
+ const primaryKeys = await this.introspectPrimaryKeys(tableName);
426
+ const uniqueConstraints = await this.introspectUniqueConstraints(tableName);
427
+ for (const col of columns) {
428
+ if (primaryKeys.includes(col.name)) col.isPrimary = true;
429
+ if (uniqueConstraints.includes(col.name)) col.isUnique = true;
430
+ }
431
+ tables[tableName] = { name: tableName, columns, foreignKeys, primaryKeys };
432
+ }
433
+ return { tables };
434
+ }
435
+ // ===================================
436
+ // Internal helpers
437
+ // ===================================
438
+ /** Expose the underlying Knex instance for advanced usage. */
439
+ getKnex() {
440
+ return this.knex;
441
+ }
442
+ getBuilder(object, options) {
443
+ let builder = this.knex(object);
444
+ if (options?.transaction) {
445
+ builder = builder.transacting(options.transaction);
446
+ }
447
+ return builder;
448
+ }
449
+ // ── Filter helpers ──────────────────────────────────────────────────────────
450
+ applyFilters(builder, filters) {
451
+ if (!filters) return;
452
+ if (!Array.isArray(filters) && typeof filters === "object") {
453
+ const hasMongoOperators = Object.keys(filters).some(
454
+ (k) => k.startsWith("$") || typeof filters[k] === "object" && filters[k] !== null && Object.keys(filters[k]).some((op) => op.startsWith("$"))
455
+ );
456
+ if (hasMongoOperators) {
457
+ this.applyFilterCondition(builder, filters);
458
+ return;
459
+ }
460
+ for (const [key, value] of Object.entries(filters)) {
461
+ if (["filters", "sort", "limit", "skip", "offset", "fields", "orderBy"].includes(key)) continue;
462
+ builder.where(key, value);
463
+ }
464
+ return;
465
+ }
466
+ if (!Array.isArray(filters) || filters.length === 0) return;
467
+ let nextJoin = "and";
468
+ for (const item of filters) {
469
+ if (typeof item === "string") {
470
+ if (item.toLowerCase() === "or") nextJoin = "or";
471
+ else if (item.toLowerCase() === "and") nextJoin = "and";
472
+ continue;
473
+ }
474
+ if (Array.isArray(item)) {
475
+ const [fieldRaw, op, value] = item;
476
+ const isCriterion = typeof fieldRaw === "string" && typeof op === "string";
477
+ if (isCriterion) {
478
+ const field = this.mapSortField(fieldRaw);
479
+ const apply = (b) => {
480
+ const method = nextJoin === "or" ? "orWhere" : "where";
481
+ const methodIn = nextJoin === "or" ? "orWhereIn" : "whereIn";
482
+ const methodNotIn = nextJoin === "or" ? "orWhereNotIn" : "whereNotIn";
483
+ if (op === "contains") {
484
+ b[method](field, "like", `%${value}%`);
485
+ return;
486
+ }
487
+ switch (op) {
488
+ case "=":
489
+ b[method](field, value);
490
+ break;
491
+ case "!=":
492
+ b[method](field, "<>", value);
493
+ break;
494
+ case "in":
495
+ b[methodIn](field, value);
496
+ break;
497
+ case "nin":
498
+ b[methodNotIn](field, value);
499
+ break;
500
+ default:
501
+ b[method](field, op, value);
502
+ }
503
+ };
504
+ apply(builder);
505
+ } else {
506
+ const method = nextJoin === "or" ? "orWhere" : "where";
507
+ builder[method]((qb) => {
508
+ this.applyFilters(qb, item);
509
+ });
510
+ }
511
+ nextJoin = "and";
512
+ }
513
+ }
514
+ }
515
+ applyFilterCondition(builder, condition, logicalOp = "and") {
516
+ if (!condition || typeof condition !== "object") return;
517
+ for (const [key, value] of Object.entries(condition)) {
518
+ if (key === "$and" && Array.isArray(value)) {
519
+ builder.where((qb) => {
520
+ for (const sub of value) {
521
+ qb.where((subQb) => {
522
+ this.applyFilterCondition(subQb, sub, "and");
523
+ });
524
+ }
525
+ });
526
+ } else if (key === "$or" && Array.isArray(value)) {
527
+ const method = logicalOp === "or" ? "orWhere" : "where";
528
+ builder[method]((qb) => {
529
+ for (const sub of value) {
530
+ qb.orWhere((subQb) => {
531
+ this.applyFilterCondition(subQb, sub, "or");
532
+ });
533
+ }
534
+ });
535
+ } else if (typeof value === "object" && value !== null && !Array.isArray(value)) {
536
+ const field = this.mapSortField(key);
537
+ for (const [op, opValue] of Object.entries(value)) {
538
+ const method = logicalOp === "or" ? "orWhere" : "where";
539
+ switch (op) {
540
+ case "$eq":
541
+ builder[method](field, opValue);
542
+ break;
543
+ case "$ne":
544
+ builder[method](field, "<>", opValue);
545
+ break;
546
+ case "$gt":
547
+ builder[method](field, ">", opValue);
548
+ break;
549
+ case "$gte":
550
+ builder[method](field, ">=", opValue);
551
+ break;
552
+ case "$lt":
553
+ builder[method](field, "<", opValue);
554
+ break;
555
+ case "$lte":
556
+ builder[method](field, "<=", opValue);
557
+ break;
558
+ case "$in": {
559
+ const mIn = logicalOp === "or" ? "orWhereIn" : "whereIn";
560
+ builder[mIn](field, opValue);
561
+ break;
562
+ }
563
+ case "$nin": {
564
+ const mNotIn = logicalOp === "or" ? "orWhereNotIn" : "whereNotIn";
565
+ builder[mNotIn](field, opValue);
566
+ break;
567
+ }
568
+ case "$contains":
569
+ builder[method](field, "like", `%${opValue}%`);
570
+ break;
571
+ default:
572
+ builder[method](field, opValue);
573
+ }
574
+ }
575
+ } else {
576
+ const field = this.mapSortField(key);
577
+ const method = logicalOp === "or" ? "orWhere" : "where";
578
+ builder[method](field, value);
579
+ }
580
+ }
581
+ }
582
+ // ── Field mapping ───────────────────────────────────────────────────────────
583
+ mapSortField(field) {
584
+ if (field === "createdAt") return "created_at";
585
+ if (field === "updatedAt") return "updated_at";
586
+ return field;
587
+ }
588
+ mapAggregateFunc(func) {
589
+ switch (func) {
590
+ case "count":
591
+ return "count";
592
+ case "sum":
593
+ return "sum";
594
+ case "avg":
595
+ return "avg";
596
+ case "min":
597
+ return "min";
598
+ case "max":
599
+ return "max";
600
+ default:
601
+ throw new Error(`Unsupported aggregate function: ${func}`);
602
+ }
603
+ }
604
+ // ── Window function builder ─────────────────────────────────────────────────
605
+ buildWindowFunction(spec) {
606
+ const func = spec.function.toUpperCase();
607
+ let sql = `${func}()`;
608
+ const overParts = [];
609
+ if (spec.partitionBy && Array.isArray(spec.partitionBy) && spec.partitionBy.length > 0) {
610
+ const partitionFields = spec.partitionBy.map((f) => this.mapSortField(f)).join(", ");
611
+ overParts.push(`PARTITION BY ${partitionFields}`);
612
+ }
613
+ if (spec.orderBy && Array.isArray(spec.orderBy) && spec.orderBy.length > 0) {
614
+ const orderFields = spec.orderBy.map((s) => {
615
+ const field = this.mapSortField(s.field);
616
+ const order = (s.order || "asc").toUpperCase();
617
+ return `${field} ${order}`;
618
+ }).join(", ");
619
+ overParts.push(`ORDER BY ${orderFields}`);
620
+ }
621
+ sql += overParts.length > 0 ? ` OVER (${overParts.join(" ")})` : ` OVER ()`;
622
+ return sql;
623
+ }
624
+ // ── Column creation helper ──────────────────────────────────────────────────
625
+ createColumn(table, name, field) {
626
+ if (field.multiple) {
627
+ table.json(name);
628
+ return;
629
+ }
630
+ const type = field.type || "string";
631
+ let col;
632
+ switch (type) {
633
+ case "string":
634
+ case "email":
635
+ case "url":
636
+ case "phone":
637
+ case "password":
638
+ col = table.string(name);
639
+ break;
640
+ case "text":
641
+ case "textarea":
642
+ case "html":
643
+ case "markdown":
644
+ col = table.text(name);
645
+ break;
646
+ case "integer":
647
+ case "int":
648
+ col = table.integer(name);
649
+ break;
650
+ case "float":
651
+ case "number":
652
+ case "currency":
653
+ case "percent":
654
+ col = table.float(name);
655
+ break;
656
+ case "boolean":
657
+ col = table.boolean(name);
658
+ break;
659
+ case "date":
660
+ col = table.date(name);
661
+ break;
662
+ case "datetime":
663
+ col = table.timestamp(name);
664
+ break;
665
+ case "time":
666
+ col = table.time(name);
667
+ break;
668
+ case "json":
669
+ case "object":
670
+ case "array":
671
+ case "image":
672
+ case "file":
673
+ case "avatar":
674
+ case "location":
675
+ col = table.json(name);
676
+ break;
677
+ case "lookup":
678
+ col = table.string(name);
679
+ if (field.reference_to) {
680
+ table.foreign(name).references("id").inTable(field.reference_to);
681
+ }
682
+ break;
683
+ case "summary":
684
+ col = table.float(name);
685
+ break;
686
+ case "auto_number":
687
+ col = table.string(name);
688
+ break;
689
+ case "formula":
690
+ return;
691
+ // Virtual — no column
692
+ default:
693
+ col = table.string(name);
694
+ }
695
+ if (col) {
696
+ if (field.unique) col.unique();
697
+ if (field.required) col.notNullable();
698
+ }
699
+ }
700
+ // ── Database helpers ────────────────────────────────────────────────────────
701
+ async ensureDatabaseExists() {
702
+ if (!this.isPostgres) return;
703
+ try {
704
+ await this.knex.raw("SELECT 1");
705
+ } catch (e) {
706
+ if (e.code === "3D000") {
707
+ await this.createDatabase();
708
+ } else {
709
+ throw e;
710
+ }
711
+ }
712
+ }
713
+ async createDatabase() {
714
+ const config = this.config;
715
+ const connection = config.connection;
716
+ let dbName = "";
717
+ const adminConfig = { ...config };
718
+ if (typeof connection === "string") {
719
+ const url = new URL(connection);
720
+ dbName = url.pathname.slice(1);
721
+ url.pathname = "/postgres";
722
+ adminConfig.connection = url.toString();
723
+ } else {
724
+ dbName = connection.database;
725
+ adminConfig.connection = { ...connection, database: "postgres" };
726
+ }
727
+ const adminKnex = knex(adminConfig);
728
+ try {
729
+ await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
730
+ } finally {
731
+ await adminKnex.destroy();
732
+ }
733
+ }
734
+ isJsonField(type, field) {
735
+ return ["json", "object", "array", "image", "file", "avatar", "location"].includes(type) || field.multiple;
736
+ }
737
+ // ── SQLite serialisation ────────────────────────────────────────────────────
738
+ formatInput(object, data) {
739
+ if (!this.isSqlite) return data;
740
+ const fields = this.jsonFields[object];
741
+ if (!fields || fields.length === 0) return data;
742
+ const copy = { ...data };
743
+ for (const field of fields) {
744
+ if (copy[field] !== void 0 && typeof copy[field] === "object" && copy[field] !== null) {
745
+ copy[field] = JSON.stringify(copy[field]);
746
+ }
747
+ }
748
+ return copy;
749
+ }
750
+ formatOutput(object, data) {
751
+ if (!data) return data;
752
+ if (this.isSqlite) {
753
+ const jsonFields = this.jsonFields[object];
754
+ if (jsonFields && jsonFields.length > 0) {
755
+ for (const field of jsonFields) {
756
+ if (data[field] !== void 0 && typeof data[field] === "string") {
757
+ try {
758
+ data[field] = JSON.parse(data[field]);
759
+ } catch {
760
+ }
761
+ }
762
+ }
763
+ }
764
+ const booleanFields = this.booleanFields[object];
765
+ if (booleanFields && booleanFields.length > 0) {
766
+ for (const field of booleanFields) {
767
+ if (data[field] !== void 0 && data[field] !== null) {
768
+ data[field] = Boolean(data[field]);
769
+ }
770
+ }
771
+ }
772
+ }
773
+ return data;
774
+ }
775
+ // ── Introspection internals ─────────────────────────────────────────────────
776
+ async introspectColumns(tableName) {
777
+ const columnInfo = await this.knex(tableName).columnInfo();
778
+ const columns = [];
779
+ for (const [colName, info] of Object.entries(columnInfo)) {
780
+ let type = "string";
781
+ let maxLength;
782
+ if (this.isSqlite) {
783
+ type = info.type?.toLowerCase() || "string";
784
+ } else {
785
+ type = info.type || "string";
786
+ }
787
+ if (info.maxLength) {
788
+ maxLength = info.maxLength;
789
+ }
790
+ columns.push({
791
+ name: colName,
792
+ type,
793
+ nullable: info.nullable !== false,
794
+ defaultValue: info.defaultValue,
795
+ isPrimary: false,
796
+ isUnique: false,
797
+ maxLength
798
+ });
799
+ }
800
+ return columns;
801
+ }
802
+ async introspectForeignKeys(tableName) {
803
+ const foreignKeys = [];
804
+ try {
805
+ if (this.isPostgres) {
806
+ const result = await this.knex.raw(
807
+ `
808
+ SELECT
809
+ kcu.column_name,
810
+ ccu.table_name AS referenced_table,
811
+ ccu.column_name AS referenced_column,
812
+ tc.constraint_name
813
+ FROM information_schema.table_constraints AS tc
814
+ JOIN information_schema.key_column_usage AS kcu
815
+ ON tc.constraint_name = kcu.constraint_name
816
+ AND tc.table_schema = kcu.table_schema
817
+ JOIN information_schema.constraint_column_usage AS ccu
818
+ ON ccu.constraint_name = tc.constraint_name
819
+ AND ccu.table_schema = tc.table_schema
820
+ WHERE tc.constraint_type = 'FOREIGN KEY'
821
+ AND tc.table_name = ?
822
+ `,
823
+ [tableName]
824
+ );
825
+ for (const row of result.rows) {
826
+ foreignKeys.push({
827
+ columnName: row.column_name,
828
+ referencedTable: row.referenced_table,
829
+ referencedColumn: row.referenced_column,
830
+ constraintName: row.constraint_name
831
+ });
832
+ }
833
+ } else if (this.isMysql) {
834
+ const result = await this.knex.raw(
835
+ `
836
+ SELECT
837
+ COLUMN_NAME as column_name,
838
+ REFERENCED_TABLE_NAME as referenced_table,
839
+ REFERENCED_COLUMN_NAME as referenced_column,
840
+ CONSTRAINT_NAME as constraint_name
841
+ FROM information_schema.KEY_COLUMN_USAGE
842
+ WHERE TABLE_SCHEMA = DATABASE()
843
+ AND TABLE_NAME = ?
844
+ AND REFERENCED_TABLE_NAME IS NOT NULL
845
+ `,
846
+ [tableName]
847
+ );
848
+ for (const row of result[0]) {
849
+ foreignKeys.push({
850
+ columnName: row.column_name,
851
+ referencedTable: row.referenced_table,
852
+ referencedColumn: row.referenced_column,
853
+ constraintName: row.constraint_name
854
+ });
855
+ }
856
+ } else if (this.isSqlite) {
857
+ const tableExistsResult = await this.knex.raw(
858
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
859
+ [tableName]
860
+ );
861
+ if (!Array.isArray(tableExistsResult) || tableExistsResult.length === 0) {
862
+ return foreignKeys;
863
+ }
864
+ const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
865
+ const result = await this.knex.raw(`PRAGMA foreign_key_list(${safeTableName})`);
866
+ for (const row of result) {
867
+ foreignKeys.push({
868
+ columnName: row.from,
869
+ referencedTable: row.table,
870
+ referencedColumn: row.to,
871
+ constraintName: `fk_${tableName}_${row.from}`
872
+ });
873
+ }
874
+ }
875
+ } catch {
876
+ }
877
+ return foreignKeys;
878
+ }
879
+ async introspectPrimaryKeys(tableName) {
880
+ const primaryKeys = [];
881
+ try {
882
+ if (this.isPostgres) {
883
+ const result = await this.knex.raw(
884
+ `
885
+ SELECT a.attname as column_name
886
+ FROM pg_index i
887
+ JOIN pg_attribute a ON a.attrelid = i.indrelid
888
+ AND a.attnum = ANY(i.indkey)
889
+ WHERE i.indrelid = ?::regclass
890
+ AND i.indisprimary
891
+ `,
892
+ [tableName]
893
+ );
894
+ for (const row of result.rows) {
895
+ primaryKeys.push(row.column_name);
896
+ }
897
+ } else if (this.isMysql) {
898
+ const result = await this.knex.raw(
899
+ `
900
+ SELECT COLUMN_NAME as column_name
901
+ FROM information_schema.KEY_COLUMN_USAGE
902
+ WHERE TABLE_SCHEMA = DATABASE()
903
+ AND TABLE_NAME = ?
904
+ AND CONSTRAINT_NAME = 'PRIMARY'
905
+ `,
906
+ [tableName]
907
+ );
908
+ for (const row of result[0]) {
909
+ primaryKeys.push(row.column_name);
910
+ }
911
+ } else if (this.isSqlite) {
912
+ const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
913
+ const tablesResult = await this.knex.raw("SELECT name FROM sqlite_master WHERE type = 'table'");
914
+ const tableNames = Array.isArray(tablesResult) ? tablesResult.map((row) => row.name) : [];
915
+ if (!tableNames.includes(safeTableName)) {
916
+ return primaryKeys;
917
+ }
918
+ const result = await this.knex.raw(`PRAGMA table_info(${safeTableName})`);
919
+ for (const row of result) {
920
+ if (row.pk === 1) {
921
+ primaryKeys.push(row.name);
922
+ }
923
+ }
924
+ }
925
+ } catch {
926
+ }
927
+ return primaryKeys;
928
+ }
929
+ async introspectUniqueConstraints(tableName) {
930
+ const uniqueColumns = [];
931
+ try {
932
+ if (this.isPostgres) {
933
+ const result = await this.knex.raw(
934
+ `
935
+ SELECT c.column_name
936
+ FROM information_schema.table_constraints tc
937
+ JOIN information_schema.constraint_column_usage AS ccu
938
+ ON tc.constraint_schema = ccu.constraint_schema
939
+ AND tc.constraint_name = ccu.constraint_name
940
+ WHERE tc.constraint_type = 'UNIQUE'
941
+ AND tc.table_name = ?
942
+ `,
943
+ [tableName]
944
+ );
945
+ for (const row of result.rows) {
946
+ uniqueColumns.push(row.column_name);
947
+ }
948
+ } else if (this.isMysql) {
949
+ const result = await this.knex.raw(
950
+ `
951
+ SELECT COLUMN_NAME
952
+ FROM information_schema.TABLE_CONSTRAINTS tc
953
+ JOIN information_schema.KEY_COLUMN_USAGE kcu
954
+ USING (CONSTRAINT_NAME, TABLE_SCHEMA, TABLE_NAME)
955
+ WHERE CONSTRAINT_TYPE = 'UNIQUE'
956
+ AND TABLE_SCHEMA = DATABASE()
957
+ AND TABLE_NAME = ?
958
+ `,
959
+ [tableName]
960
+ );
961
+ for (const row of result[0]) {
962
+ uniqueColumns.push(row.COLUMN_NAME);
963
+ }
964
+ } else if (this.isSqlite) {
965
+ const safeTableName = tableName.replace(/[^a-zA-Z0-9_]/g, "");
966
+ const tablesResult = await this.knex.raw("SELECT name FROM sqlite_master WHERE type = 'table'");
967
+ const tableNames = Array.isArray(tablesResult) ? tablesResult.map((row) => row.name) : [];
968
+ if (!tableNames.includes(safeTableName)) {
969
+ return uniqueColumns;
970
+ }
971
+ const indexes = await this.knex.raw(`PRAGMA index_list(${safeTableName})`);
972
+ for (const idx of indexes) {
973
+ if (idx.unique === 1) {
974
+ const info = await this.knex.raw(`PRAGMA index_info(${idx.name})`);
975
+ if (info.length === 1) {
976
+ uniqueColumns.push(info[0].name);
977
+ }
978
+ }
979
+ }
980
+ }
981
+ } catch {
982
+ }
983
+ return uniqueColumns;
984
+ }
985
+ };
986
+
987
+ // src/index.ts
988
+ var index_default = {
989
+ id: "com.objectstack.driver.sql",
990
+ version: "1.0.0",
991
+ onEnable: async (context) => {
992
+ const { logger, config, drivers } = context;
993
+ logger.info("[SQL Driver] Initializing...");
994
+ if (drivers) {
995
+ const driver = new SqlDriver(config);
996
+ drivers.register(driver);
997
+ logger.info(`[SQL Driver] Registered driver: ${driver.name}`);
998
+ } else {
999
+ logger.warn("[SQL Driver] No driver registry found in context.");
1000
+ }
1001
+ }
1002
+ };
1003
+ export {
1004
+ SqlDriver,
1005
+ index_default as default
1006
+ };
1007
+ //# sourceMappingURL=index.mjs.map