@objectstack/driver-sql 3.2.9 → 3.3.0

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