@objectstack/driver-sql 3.2.9 → 3.3.1

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.
@@ -93,7 +93,7 @@ describe('SqlDriver (SQLite Integration)', () => {
93
93
  });
94
94
 
95
95
  it('should count objects', async () => {
96
- const count = await driver.count('users', { age: 17 } as any);
96
+ const count = await driver.count('users', { where: { age: 17 } } as any);
97
97
  expect(count).toBe(2);
98
98
  });
99
99
 
package/src/sql-driver.ts CHANGED
@@ -3,12 +3,12 @@
3
3
  /**
4
4
  * SQL Driver for ObjectStack
5
5
  *
6
- * Implements the standard DriverInterface from @objectstack/spec via Knex.js.
6
+ * Implements the standard IDataDriver from @objectstack/spec via Knex.js.
7
7
  * Supports PostgreSQL, MySQL, SQLite, and other SQL databases.
8
8
  */
9
9
 
10
- import type { QueryInput, DriverOptions } from '@objectstack/spec/data';
11
- import type { DriverInterface } from '@objectstack/core';
10
+ import type { QueryAST, DriverOptions } from '@objectstack/spec/data';
11
+ import type { IDataDriver } from '@objectstack/spec/contracts';
12
12
  import knex, { Knex } from 'knex';
13
13
  import { nanoid } from 'nanoid';
14
14
 
@@ -60,47 +60,80 @@ export type SqlDriverConfig = Knex.Config;
60
60
  /**
61
61
  * SQL Driver for ObjectStack.
62
62
  *
63
- * Implements the DriverInterface contract via Knex.js for optimal SQL
63
+ * Implements the IDataDriver contract via Knex.js for optimal SQL
64
64
  * generation against PostgreSQL, MySQL, SQLite and other SQL databases.
65
65
  */
66
- export class SqlDriver implements DriverInterface {
67
- // DriverInterface metadata
68
- public readonly name = 'com.objectstack.driver.sql';
69
- public readonly version = '1.0.0';
66
+ export class SqlDriver implements IDataDriver {
67
+ // IDataDriver metadata
68
+ public readonly name: string = 'com.objectstack.driver.sql';
69
+ public readonly version: string = '1.0.0';
70
70
  public readonly supports = {
71
+ // Basic CRUD Operations
72
+ create: true,
73
+ read: true,
74
+ update: true,
75
+ delete: true,
76
+
77
+ // Bulk Operations
78
+ bulkCreate: true,
79
+ bulkUpdate: true,
80
+ bulkDelete: true,
81
+
82
+ // Transaction & Connection Management
71
83
  transactions: true,
72
- joins: true,
73
- fullTextSearch: false,
74
- jsonFields: true,
75
- arrayFields: true,
84
+ savepoints: false,
85
+
86
+ // Query Operations
76
87
  queryFilters: true,
77
88
  queryAggregations: true,
78
89
  querySorting: true,
79
90
  queryPagination: true,
80
91
  queryWindowFunctions: true,
81
92
  querySubqueries: true,
93
+ queryCTE: false,
94
+ joins: true,
95
+
96
+ // Advanced Features
97
+ fullTextSearch: false,
98
+ jsonQuery: false,
99
+ geospatialQuery: false,
100
+ streaming: false,
101
+ jsonFields: true,
102
+ arrayFields: true,
103
+ vectorSearch: false,
104
+
105
+ // Schema Management
106
+ schemaSync: true,
107
+ batchSchemaSync: false,
108
+ migrations: false,
109
+ indexes: false,
110
+
111
+ // Performance & Optimization
112
+ connectionPooling: true,
113
+ preparedStatements: true,
114
+ queryCache: false,
82
115
  };
83
116
 
84
- private knex: Knex;
85
- private config: Knex.Config;
86
- private jsonFields: Record<string, string[]> = {};
87
- private booleanFields: Record<string, string[]> = {};
88
- private tablesWithTimestamps: Set<string> = new Set();
117
+ protected knex: Knex;
118
+ protected config: Knex.Config;
119
+ protected jsonFields: Record<string, string[]> = {};
120
+ protected booleanFields: Record<string, string[]> = {};
121
+ protected tablesWithTimestamps: Set<string> = new Set();
89
122
 
90
123
  /** Whether the underlying database is a SQLite variant (sqlite3 or better-sqlite3). */
91
- private get isSqlite(): boolean {
124
+ protected get isSqlite(): boolean {
92
125
  const c = (this.config as any).client;
93
126
  return c === 'sqlite3' || c === 'better-sqlite3';
94
127
  }
95
128
 
96
129
  /** Whether the underlying database is PostgreSQL. */
97
- private get isPostgres(): boolean {
130
+ protected get isPostgres(): boolean {
98
131
  const c = (this.config as any).client;
99
132
  return c === 'pg' || c === 'postgresql';
100
133
  }
101
134
 
102
135
  /** Whether the underlying database is MySQL. */
103
- private get isMysql(): boolean {
136
+ protected get isMysql(): boolean {
104
137
  const c = (this.config as any).client;
105
138
  return c === 'mysql' || c === 'mysql2';
106
139
  }
@@ -135,7 +168,7 @@ export class SqlDriver implements DriverInterface {
135
168
  // CRUD — DriverInterface core
136
169
  // ===================================
137
170
 
138
- async find(object: string, query: QueryInput, options?: DriverOptions): Promise<any[]> {
171
+ async find(object: string, query: QueryAST, options?: DriverOptions): Promise<any[]> {
139
172
  const builder = this.getBuilder(object, options);
140
173
 
141
174
  // SELECT
@@ -145,30 +178,23 @@ export class SqlDriver implements DriverInterface {
145
178
  builder.select('*');
146
179
  }
147
180
 
148
- // WHERE — support both `where` (standard) and `filters` (legacy)
149
- const filterCondition = query.where || (query as any).filters;
150
- if (filterCondition) {
151
- this.applyFilters(builder, filterCondition);
181
+ // WHERE
182
+ if (query.where) {
183
+ this.applyFilters(builder, query.where);
152
184
  }
153
185
 
154
- // ORDER BY — support both `orderBy` (standard) and `sort` (legacy)
155
- const sortArray = query.orderBy || (query as any).sort;
156
- if (sortArray && Array.isArray(sortArray)) {
157
- for (const item of sortArray) {
158
- const field = item.field || item[0];
159
- const dir = item.order || item[1] || 'asc';
160
- if (field) {
161
- builder.orderBy(this.mapSortField(field), dir);
186
+ // ORDER BY
187
+ if (query.orderBy && Array.isArray(query.orderBy)) {
188
+ for (const item of query.orderBy) {
189
+ if (item.field) {
190
+ builder.orderBy(this.mapSortField(item.field), item.order || 'asc');
162
191
  }
163
192
  }
164
193
  }
165
194
 
166
- // PAGINATION — support both offset/limit (standard) and skip/top (legacy)
167
- const offsetValue = query.offset ?? (query as any).skip;
168
- const limitValue = query.limit ?? (query as any).top;
169
-
170
- if (offsetValue !== undefined) builder.offset(offsetValue);
171
- if (limitValue !== undefined) builder.limit(limitValue);
195
+ // PAGINATION
196
+ if (query.offset !== undefined) builder.offset(query.offset);
197
+ if (query.limit !== undefined) builder.limit(query.limit);
172
198
 
173
199
  let results: any[];
174
200
  try {
@@ -196,7 +222,7 @@ export class SqlDriver implements DriverInterface {
196
222
  return results;
197
223
  }
198
224
 
199
- async findOne(object: string, query: QueryInput, options?: DriverOptions): Promise<any> {
225
+ async findOne(object: string, query: QueryAST, options?: DriverOptions): Promise<any> {
200
226
  // When called with a string/number id fall back gracefully
201
227
  if (typeof query === 'string' || typeof query === 'number') {
202
228
  const res = await this.getBuilder(object, options).where('id', query).first();
@@ -211,6 +237,18 @@ export class SqlDriver implements DriverInterface {
211
237
  return null;
212
238
  }
213
239
 
240
+ /**
241
+ * Stream records matching a structured query.
242
+ * NOTE: Current implementation fetches all results then yields them.
243
+ * TODO: Use Knex .stream() for true cursor-based streaming on large datasets.
244
+ */
245
+ async *findStream(object: string, query: QueryAST, options?: DriverOptions): AsyncGenerator<Record<string, any>> {
246
+ const results = await this.find(object, query, options);
247
+ for (const row of results) {
248
+ yield row;
249
+ }
250
+ }
251
+
214
252
  async create(object: string, data: Record<string, any>, options?: DriverOptions): Promise<any> {
215
253
  const { _id, ...rest } = data;
216
254
  const toInsert = { ...rest };
@@ -247,13 +285,34 @@ export class SqlDriver implements DriverInterface {
247
285
  return this.formatOutput(object, updated) || null;
248
286
  }
249
287
 
250
- async delete(object: string, id: string | number, options?: DriverOptions): Promise<any> {
288
+ async upsert(object: string, data: Record<string, any>, conflictKeys?: string[], options?: DriverOptions): Promise<Record<string, any>> {
289
+ const { _id, ...rest } = data;
290
+ const toUpsert = { ...rest };
291
+
292
+ if (_id !== undefined && toUpsert.id === undefined) {
293
+ toUpsert.id = _id;
294
+ } else if (toUpsert.id === undefined) {
295
+ toUpsert.id = nanoid(DEFAULT_ID_LENGTH);
296
+ }
297
+
298
+ const formatted = this.formatInput(object, toUpsert);
299
+ const mergeKeys = conflictKeys && conflictKeys.length > 0 ? conflictKeys : ['id'];
300
+
251
301
  const builder = this.getBuilder(object, options);
252
- return await builder.where('id', id).delete();
302
+ await builder.insert(formatted).onConflict(mergeKeys).merge();
303
+
304
+ const result = await this.getBuilder(object, options).where('id', toUpsert.id).first();
305
+ return this.formatOutput(object, result) || toUpsert;
306
+ }
307
+
308
+ async delete(object: string, id: string | number, options?: DriverOptions): Promise<boolean> {
309
+ const builder = this.getBuilder(object, options);
310
+ const count = await builder.where('id', id).delete();
311
+ return count > 0;
253
312
  }
254
313
 
255
314
  // ===================================
256
- // Optional bulk & batch
315
+ // Bulk & Batch Operations
257
316
  // ===================================
258
317
 
259
318
  async bulkCreate(object: string, data: any[], options?: DriverOptions): Promise<any> {
@@ -261,38 +320,50 @@ export class SqlDriver implements DriverInterface {
261
320
  return await builder.insert(data).returning('*');
262
321
  }
263
322
 
264
- async updateMany(object: string, query: QueryInput, data: any, options?: DriverOptions): Promise<any> {
323
+ /**
324
+ * Batch-update multiple records by ID.
325
+ * NOTE: Current implementation performs sequential updates for correctness.
326
+ * TODO: Optimize with SQL CASE statements or batched transactions for performance.
327
+ */
328
+ async bulkUpdate(object: string, updates: Array<{ id: string | number; data: Record<string, any> }>, options?: DriverOptions): Promise<Record<string, any>[]> {
329
+ const results: Record<string, any>[] = [];
330
+ for (const { id, data } of updates) {
331
+ const updated = await this.update(object, id, data, options);
332
+ if (updated) results.push(updated);
333
+ }
334
+ return results;
335
+ }
336
+
337
+ async bulkDelete(object: string, ids: Array<string | number>, options?: DriverOptions): Promise<void> {
338
+ const builder = this.getBuilder(object, options);
339
+ await builder.whereIn('id', ids).delete();
340
+ }
341
+
342
+ async updateMany(object: string, query: QueryAST, data: any, options?: DriverOptions): Promise<number> {
265
343
  const builder = this.getBuilder(object, options);
266
- const filters = query.where || (query as any).filters || query;
267
- if (filters) this.applyFilters(builder, filters);
344
+ if (query.where) this.applyFilters(builder, query.where);
268
345
  const count = await builder.update(data);
269
- return { modifiedCount: count || 0 };
346
+ return count || 0;
270
347
  }
271
348
 
272
- async deleteMany(object: string, query: QueryInput, options?: DriverOptions): Promise<any> {
349
+ async deleteMany(object: string, query: QueryAST, options?: DriverOptions): Promise<number> {
273
350
  const builder = this.getBuilder(object, options);
274
- const filters = query.where || (query as any).filters || query;
275
- if (filters) this.applyFilters(builder, filters);
351
+ if (query.where) this.applyFilters(builder, query.where);
276
352
  const count = await builder.delete();
277
- return { deletedCount: count || 0 };
353
+ return count || 0;
278
354
  }
279
355
 
280
- async count(object: string, query: QueryInput, options?: DriverOptions): Promise<number> {
356
+ async count(object: string, query?: QueryAST, options?: DriverOptions): Promise<number> {
281
357
  const builder = this.getBuilder(object, options);
282
358
 
283
- let actualFilters = query as any;
284
- if (query && (query.where || (query as any).filters)) {
285
- actualFilters = query.where || (query as any).filters;
286
- }
287
-
288
- if (actualFilters) {
289
- this.applyFilters(builder, actualFilters);
359
+ if (query?.where) {
360
+ this.applyFilters(builder, query.where);
290
361
  }
291
362
 
292
363
  const result = await builder.count<{ count: number }[]>('* as count');
293
364
  if (result && result.length > 0) {
294
365
  const row: any = result[0];
295
- return Number(row.count || row['count(*)']);
366
+ return Number(row.count ?? row['count(*)'] ?? 0);
296
367
  }
297
368
  return 0;
298
369
  }
@@ -322,12 +393,24 @@ export class SqlDriver implements DriverInterface {
322
393
  return await this.knex.transaction();
323
394
  }
324
395
 
396
+ /** IDataDriver standard */
397
+ async commit(transaction: unknown): Promise<void> {
398
+ await (transaction as Knex.Transaction).commit();
399
+ }
400
+
401
+ /** IDataDriver standard */
402
+ async rollback(transaction: unknown): Promise<void> {
403
+ await (transaction as Knex.Transaction).rollback();
404
+ }
405
+
406
+ /** @deprecated Use commit() instead */
325
407
  async commitTransaction(trx: Knex.Transaction): Promise<void> {
326
- await trx.commit();
408
+ await this.commit(trx);
327
409
  }
328
410
 
411
+ /** @deprecated Use rollback() instead */
329
412
  async rollbackTransaction(trx: Knex.Transaction): Promise<void> {
330
- await trx.rollback();
413
+ await this.rollback(trx);
331
414
  }
332
415
 
333
416
  // ===================================
@@ -416,6 +499,11 @@ export class SqlDriver implements DriverInterface {
416
499
  // Query Plan Analysis
417
500
  // ===================================
418
501
 
502
+ /** IDataDriver standard: analyze query performance */
503
+ async explain(object: string, query: any, options?: DriverOptions): Promise<any> {
504
+ return this.analyzeQuery(object, query, options);
505
+ }
506
+
419
507
  async analyzeQuery(object: string, query: any, options?: DriverOptions): Promise<any> {
420
508
  const builder = this.getBuilder(object, options);
421
509
 
@@ -476,17 +564,25 @@ export class SqlDriver implements DriverInterface {
476
564
 
477
565
  async syncSchema(object: string, schema: unknown, _options?: DriverOptions): Promise<void> {
478
566
  const objectDef = schema as { name: string; fields?: Record<string, any> };
479
- await this.initObjects([objectDef]);
567
+ // Use the physical table name (`object`) for DDL operations. The caller
568
+ // (e.g. syncRegisteredSchemas) passes the resolved tableName (e.g. 'sys_user')
569
+ // while objectDef.name may contain the FQN (e.g. 'sys__user').
570
+ await this.initObjects([{ ...objectDef, name: object }]);
571
+ }
572
+
573
+ async dropTable(object: string, _options?: DriverOptions): Promise<void> {
574
+ await this.knex.schema.dropTableIfExists(object);
480
575
  }
481
576
 
482
577
  /**
483
578
  * Batch-initialise tables from an array of object definitions.
484
579
  */
485
- async initObjects(objects: Array<{ name: string; fields?: Record<string, any> }>): Promise<void> {
580
+ async initObjects(objects: Array<{ name: string; tableName?: string; fields?: Record<string, any> }>): Promise<void> {
486
581
  await this.ensureDatabaseExists();
487
582
 
488
583
  for (const obj of objects) {
489
- const tableName = obj.name;
584
+ // Prefer explicit tableName (physical name) over obj.name (which may be a FQN).
585
+ const tableName = obj.tableName || obj.name;
490
586
 
491
587
  const jsonCols: string[] = [];
492
588
  const booleanCols: string[] = [];
@@ -516,6 +612,11 @@ export class SqlDriver implements DriverInterface {
516
612
  }
517
613
  }
518
614
 
615
+ // Columns created unconditionally by initObjects — skip them when
616
+ // iterating obj.fields to avoid duplicate-column errors (e.g. SQLite
617
+ // rejects CREATE TABLE with two columns of the same name).
618
+ const builtinColumns = new Set(['id', 'created_at', 'updated_at']);
619
+
519
620
  if (!exists) {
520
621
  await this.knex.schema.createTable(tableName, (table) => {
521
622
  table.string('id').primary();
@@ -523,6 +624,7 @@ export class SqlDriver implements DriverInterface {
523
624
  table.timestamp('updated_at').defaultTo(this.knex.fn.now());
524
625
  if (obj.fields) {
525
626
  for (const [name, field] of Object.entries(obj.fields)) {
627
+ if (builtinColumns.has(name)) continue;
526
628
  this.createColumn(table, name, field);
527
629
  }
528
630
  }
@@ -609,7 +711,7 @@ export class SqlDriver implements DriverInterface {
609
711
  return this.knex;
610
712
  }
611
713
 
612
- private getBuilder(object: string, options?: DriverOptions) {
714
+ protected getBuilder(object: string, options?: DriverOptions) {
613
715
  let builder = this.knex(object);
614
716
  if (options?.transaction) {
615
717
  builder = builder.transacting(options.transaction as Knex.Transaction);
@@ -619,7 +721,7 @@ export class SqlDriver implements DriverInterface {
619
721
 
620
722
  // ── Filter helpers ──────────────────────────────────────────────────────────
621
723
 
622
- private applyFilters(builder: Knex.QueryBuilder, filters: any) {
724
+ protected applyFilters(builder: Knex.QueryBuilder, filters: any) {
623
725
  if (!filters) return;
624
726
 
625
727
  if (!Array.isArray(filters) && typeof filters === 'object') {
@@ -637,7 +739,7 @@ export class SqlDriver implements DriverInterface {
637
739
  }
638
740
 
639
741
  for (const [key, value] of Object.entries(filters)) {
640
- if (['filters', 'sort', 'limit', 'skip', 'offset', 'fields', 'orderBy'].includes(key)) continue;
742
+ if (['limit', 'offset', 'fields', 'orderBy'].includes(key)) continue;
641
743
  builder.where(key, value as any);
642
744
  }
643
745
  return;
@@ -700,7 +802,7 @@ export class SqlDriver implements DriverInterface {
700
802
  }
701
803
  }
702
804
 
703
- private applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp: 'and' | 'or' = 'and') {
805
+ protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp: 'and' | 'or' = 'and') {
704
806
  if (!condition || typeof condition !== 'object') return;
705
807
 
706
808
  for (const [key, value] of Object.entries(condition)) {
@@ -771,13 +873,13 @@ export class SqlDriver implements DriverInterface {
771
873
 
772
874
  // ── Field mapping ───────────────────────────────────────────────────────────
773
875
 
774
- private mapSortField(field: string): string {
876
+ protected mapSortField(field: string): string {
775
877
  if (field === 'createdAt') return 'created_at';
776
878
  if (field === 'updatedAt') return 'updated_at';
777
879
  return field;
778
880
  }
779
881
 
780
- private mapAggregateFunc(func: string): string {
882
+ protected mapAggregateFunc(func: string): string {
781
883
  switch (func) {
782
884
  case 'count':
783
885
  return 'count';
@@ -796,7 +898,7 @@ export class SqlDriver implements DriverInterface {
796
898
 
797
899
  // ── Window function builder ─────────────────────────────────────────────────
798
900
 
799
- private buildWindowFunction(spec: any): string {
901
+ protected buildWindowFunction(spec: any): string {
800
902
  const func = spec.function.toUpperCase();
801
903
  let sql = `${func}()`;
802
904
 
@@ -824,7 +926,7 @@ export class SqlDriver implements DriverInterface {
824
926
 
825
927
  // ── Column creation helper ──────────────────────────────────────────────────
826
928
 
827
- private createColumn(table: Knex.CreateTableBuilder, name: string, field: any) {
929
+ protected createColumn(table: Knex.CreateTableBuilder, name: string, field: any) {
828
930
  if (field.multiple) {
829
931
  table.json(name);
830
932
  return;
@@ -903,13 +1005,23 @@ export class SqlDriver implements DriverInterface {
903
1005
 
904
1006
  // ── Database helpers ────────────────────────────────────────────────────────
905
1007
 
906
- private async ensureDatabaseExists() {
907
- if (!this.isPostgres) return;
1008
+ protected async ensureDatabaseExists() {
1009
+ // SQLite auto-creates database files — no need to check
1010
+ if (this.isSqlite) return;
1011
+
1012
+ // Only PostgreSQL and MySQL support programmatic database creation
1013
+ if (!this.isPostgres && !this.isMysql) return;
908
1014
 
909
1015
  try {
910
1016
  await this.knex.raw('SELECT 1');
911
1017
  } catch (e: any) {
912
- if (e.code === '3D000') {
1018
+ // PostgreSQL: '3D000' = database does not exist
1019
+ // MySQL: 'ER_BAD_DB_ERROR' (errno 1049) = unknown database
1020
+ if (
1021
+ e.code === '3D000' ||
1022
+ e.code === 'ER_BAD_DB_ERROR' ||
1023
+ e.errno === 1049
1024
+ ) {
913
1025
  await this.createDatabase();
914
1026
  } else {
915
1027
  throw e;
@@ -917,37 +1029,58 @@ export class SqlDriver implements DriverInterface {
917
1029
  }
918
1030
  }
919
1031
 
920
- private async createDatabase() {
1032
+ protected async createDatabase() {
921
1033
  const config = this.config as any;
922
1034
  const connection = config.connection;
923
1035
  let dbName = '';
924
1036
  const adminConfig = { ...config };
925
1037
 
926
- if (typeof connection === 'string') {
927
- const url = new URL(connection);
928
- dbName = url.pathname.slice(1);
929
- url.pathname = '/postgres';
930
- adminConfig.connection = url.toString();
1038
+ if (this.isPostgres) {
1039
+ // PostgreSQL: connect to the 'postgres' maintenance database
1040
+ if (typeof connection === 'string') {
1041
+ const url = new URL(connection);
1042
+ dbName = url.pathname.slice(1);
1043
+ url.pathname = '/postgres';
1044
+ adminConfig.connection = url.toString();
1045
+ } else {
1046
+ dbName = connection.database;
1047
+ adminConfig.connection = { ...connection, database: 'postgres' };
1048
+ }
1049
+ } else if (this.isMysql) {
1050
+ // MySQL: connect without specifying a database
1051
+ if (typeof connection === 'string') {
1052
+ const url = new URL(connection);
1053
+ dbName = url.pathname.slice(1);
1054
+ url.pathname = '/';
1055
+ adminConfig.connection = url.toString();
1056
+ } else {
1057
+ dbName = connection.database;
1058
+ const { database: _db, ...rest } = connection;
1059
+ adminConfig.connection = rest;
1060
+ }
931
1061
  } else {
932
- dbName = connection.database;
933
- adminConfig.connection = { ...connection, database: 'postgres' };
1062
+ return; // Unsupported dialect for auto-creation
934
1063
  }
935
1064
 
936
1065
  const adminKnex = knex(adminConfig);
937
1066
  try {
938
- await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
1067
+ if (this.isPostgres) {
1068
+ await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
1069
+ } else if (this.isMysql) {
1070
+ await adminKnex.raw(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``);
1071
+ }
939
1072
  } finally {
940
1073
  await adminKnex.destroy();
941
1074
  }
942
1075
  }
943
1076
 
944
- private isJsonField(type: string, field: any): boolean {
1077
+ protected isJsonField(type: string, field: any): boolean {
945
1078
  return ['json', 'object', 'array', 'image', 'file', 'avatar', 'location'].includes(type) || field.multiple;
946
1079
  }
947
1080
 
948
1081
  // ── SQLite serialisation ────────────────────────────────────────────────────
949
1082
 
950
- private formatInput(object: string, data: any): any {
1083
+ protected formatInput(object: string, data: any): any {
951
1084
  if (!this.isSqlite) return data;
952
1085
 
953
1086
  const fields = this.jsonFields[object];
@@ -962,7 +1095,7 @@ export class SqlDriver implements DriverInterface {
962
1095
  return copy;
963
1096
  }
964
1097
 
965
- private formatOutput(object: string, data: any): any {
1098
+ protected formatOutput(object: string, data: any): any {
966
1099
  if (!data) return data;
967
1100
 
968
1101
  if (this.isSqlite) {
@@ -994,7 +1127,7 @@ export class SqlDriver implements DriverInterface {
994
1127
 
995
1128
  // ── Introspection internals ─────────────────────────────────────────────────
996
1129
 
997
- private async introspectColumns(tableName: string): Promise<IntrospectedColumn[]> {
1130
+ protected async introspectColumns(tableName: string): Promise<IntrospectedColumn[]> {
998
1131
  const columnInfo = await this.knex(tableName).columnInfo();
999
1132
  const columns: IntrospectedColumn[] = [];
1000
1133
 
@@ -1026,7 +1159,7 @@ export class SqlDriver implements DriverInterface {
1026
1159
  return columns;
1027
1160
  }
1028
1161
 
1029
- private async introspectForeignKeys(tableName: string): Promise<IntrospectedForeignKey[]> {
1162
+ protected async introspectForeignKeys(tableName: string): Promise<IntrospectedForeignKey[]> {
1030
1163
  const foreignKeys: IntrospectedForeignKey[] = [];
1031
1164
 
1032
1165
  try {
@@ -1112,7 +1245,7 @@ export class SqlDriver implements DriverInterface {
1112
1245
  return foreignKeys;
1113
1246
  }
1114
1247
 
1115
- private async introspectPrimaryKeys(tableName: string): Promise<string[]> {
1248
+ protected async introspectPrimaryKeys(tableName: string): Promise<string[]> {
1116
1249
  const primaryKeys: string[] = [];
1117
1250
 
1118
1251
  try {
@@ -1172,7 +1305,7 @@ export class SqlDriver implements DriverInterface {
1172
1305
  return primaryKeys;
1173
1306
  }
1174
1307
 
1175
- private async introspectUniqueConstraints(tableName: string): Promise<string[]> {
1308
+ protected async introspectUniqueConstraints(tableName: string): Promise<string[]> {
1176
1309
  const uniqueColumns: string[] = [];
1177
1310
 
1178
1311
  try {
package/tsconfig.json CHANGED
@@ -11,7 +11,10 @@
11
11
  "skipLibCheck": true,
12
12
  "noUnusedLocals": false,
13
13
  "noUnusedParameters": false,
14
- "forceConsistentCasingInFileNames": true
14
+ "forceConsistentCasingInFileNames": true,
15
+ "paths": {
16
+ "knex": ["./node_modules/knex/types/index.d.ts"]
17
+ }
15
18
  },
16
19
  "include": ["src/**/*"],
17
20
  "exclude": ["node_modules", "dist"]