@objectql/driver-knex 1.1.0 → 1.2.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.
package/src/index.ts CHANGED
@@ -1,10 +1,13 @@
1
- import { Driver } from '@objectql/core';
1
+ import { Driver } from '@objectql/types';
2
2
  import knex, { Knex } from 'knex';
3
3
 
4
4
  export class KnexDriver implements Driver {
5
5
  private knex: Knex;
6
+ private config: any;
7
+ private jsonFields: Record<string, string[]> = {};
6
8
 
7
9
  constructor(config: any) {
10
+ this.config = config;
8
11
  this.knex = knex(config);
9
12
  }
10
13
 
@@ -19,8 +22,6 @@ export class KnexDriver implements Driver {
19
22
  private applyFilters(builder: Knex.QueryBuilder, filters: any) {
20
23
  if (!filters || filters.length === 0) return;
21
24
 
22
- // Simple linear parser handling [cond, 'or', cond, 'and', cond]
23
- // Default join is AND.
24
25
  let nextJoin = 'and';
25
26
 
26
27
  for (const item of filters) {
@@ -31,46 +32,57 @@ export class KnexDriver implements Driver {
31
32
  }
32
33
 
33
34
  if (Array.isArray(item)) {
34
- const [field, op, value] = item;
35
-
36
- // Handle specific operators that map to different knex methods
37
- const apply = (b: any) => {
38
- // b is the builder to apply on (could be root or a where clause)
39
- // But here we call directly on builder using 'where' or 'orWhere'
40
-
41
- // Method selection
42
- let method = nextJoin === 'or' ? 'orWhere' : 'where';
43
- let methodIn = nextJoin === 'or' ? 'orWhereIn' : 'whereIn';
44
- let methodNotIn = nextJoin === 'or' ? 'orWhereNotIn' : 'whereNotIn';
45
-
46
- switch (op) {
47
- case '=': b[method](field, value); break;
48
- case '!=': b[method](field, '<>', value); break;
49
- case 'in': b[methodIn](field, value); break;
50
- case 'nin': b[methodNotIn](field, value); break;
51
- case 'contains': b[method](field, 'like', `%${value}%`); break; // Simple LIKE
52
- default: b[method](field, op, value);
53
- }
54
- };
35
+ // Heuristic to detect if it is a criterion [field, op, value] or a nested group
36
+ const [fieldRaw, op, value] = item;
37
+ const isCriterion = typeof fieldRaw === 'string' && typeof op === 'string';
55
38
 
56
- apply(builder);
39
+ if (isCriterion) {
40
+ const field = this.mapSortField(fieldRaw);
41
+ // Handle specific operators that map to different knex methods
42
+ const apply = (b: any) => {
43
+ let method = nextJoin === 'or' ? 'orWhere' : 'where';
44
+ let methodIn = nextJoin === 'or' ? 'orWhereIn' : 'whereIn';
45
+ let methodNotIn = nextJoin === 'or' ? 'orWhereNotIn' : 'whereNotIn';
46
+
47
+ // Fix for 'contains' mapping
48
+ if (op === 'contains') {
49
+ b[method](field, 'like', `%${value}%`);
50
+ return;
51
+ }
52
+
53
+ switch (op) {
54
+ case '=': b[method](field, value); break;
55
+ case '!=': b[method](field, '<>', value); break;
56
+ case 'in': b[methodIn](field, value); break;
57
+ case 'nin': b[methodNotIn](field, value); break;
58
+ default: b[method](field, op, value);
59
+ }
60
+ };
61
+ apply(builder);
62
+ } else {
63
+ // Recursive Group
64
+ const method = nextJoin === 'or' ? 'orWhere' : 'where';
65
+ builder[method]((qb) => {
66
+ this.applyFilters(qb, item);
67
+ });
68
+ }
57
69
 
58
- // Reset join to 'and' for subsequent terms unless strictly specified?
59
- // In SQL `A or B and C` means `A or (B and C)`.
60
- // If we chain `.where(A).orWhere(B).where(C)` in Knex:
61
- // It produces `... WHERE A OR B AND C`.
62
- // So linear application matches SQL precedence usually if implicit validation is ok.
63
- // But explicit AND after OR is necessary in our array format.
64
70
  nextJoin = 'and';
65
71
  }
66
72
  }
67
73
  }
68
74
 
75
+ private mapSortField(field: string): string {
76
+ if (field === 'createdAt') return 'created_at';
77
+ if (field === 'updatedAt') return 'updated_at';
78
+ return field;
79
+ }
80
+
69
81
  async find(objectName: string, query: any, options?: any): Promise<any[]> {
70
82
  const builder = this.getBuilder(objectName, options);
71
83
 
72
84
  if (query.fields) {
73
- builder.select(query.fields);
85
+ builder.select(query.fields.map((f: string) => this.mapSortField(f)));
74
86
  } else {
75
87
  builder.select('*');
76
88
  }
@@ -81,19 +93,26 @@ export class KnexDriver implements Driver {
81
93
 
82
94
  if (query.sort) {
83
95
  for (const [field, dir] of query.sort) {
84
- builder.orderBy(field, dir);
96
+ builder.orderBy(this.mapSortField(field), dir);
85
97
  }
86
98
  }
87
99
 
88
100
  if (query.skip) builder.offset(query.skip);
89
101
  if (query.limit) builder.limit(query.limit);
90
102
 
91
- return await builder;
103
+ const results = await builder;
104
+ if (this.config.client === 'sqlite3') {
105
+ for (const row of results) {
106
+ this.formatOutput(objectName, row);
107
+ }
108
+ }
109
+ return results;
92
110
  }
93
111
 
94
112
  async findOne(objectName: string, id: string | number, query?: any, options?: any) {
95
113
  if (id) {
96
- return await this.getBuilder(objectName, options).where('id', id).first();
114
+ const res = await this.getBuilder(objectName, options).where('id', id).first();
115
+ return this.formatOutput(objectName, res);
97
116
  }
98
117
  if (query) {
99
118
  const results = await this.find(objectName, { ...query, limit: 1 }, options);
@@ -103,19 +122,32 @@ export class KnexDriver implements Driver {
103
122
  }
104
123
 
105
124
  async create(objectName: string, data: any, options?: any) {
125
+ // Handle _id -> id mapping for compatibility
126
+ const { _id, ...rest } = data;
127
+ const toInsert = { ...rest };
128
+ // If _id exists and id doesn't, map _id to id
129
+ if (_id !== undefined && toInsert.id === undefined) {
130
+ toInsert.id = _id;
131
+ } else if (toInsert.id !== undefined) {
132
+ // normal case
133
+ }
134
+
106
135
  // Knex insert returns Result array (e.g. ids)
107
136
  // We want the created document.
108
- // Some DBs support .returning('*'), others don't easily.
109
- // Assuming Postgres/SQLite/Modern MySQL for now support returning.
110
137
  const builder = this.getBuilder(objectName, options);
111
- const result = await builder.insert(data).returning('*'); // This might fail on old MySQL
112
- return result[0];
138
+
139
+ const formatted = this.formatInput(objectName, toInsert);
140
+
141
+ // We should insert 'toInsert' instead of 'data'
142
+ const result = await builder.insert(formatted).returning('*');
143
+ return this.formatOutput(objectName, result[0]);
113
144
  }
114
145
 
115
146
  async update(objectName: string, id: string | number, data: any, options?: any) {
116
147
  const builder = this.getBuilder(objectName, options);
117
- await builder.where('id', id).update(data);
118
- return { id, ...data }; // Return patched data? Or fetch fresh?
148
+ const formatted = this.formatInput(objectName, data);
149
+ await builder.where('id', id).update(formatted);
150
+ return { id, ...data };
119
151
  }
120
152
 
121
153
  async delete(objectName: string, id: string | number, options?: any) {
@@ -167,5 +199,208 @@ export class KnexDriver implements Driver {
167
199
  if(filters) this.applyFilters(builder, filters);
168
200
  return await builder.delete();
169
201
  }
202
+
203
+ async init(objects: any[]): Promise<void> {
204
+ await this.ensureDatabaseExists();
205
+
206
+ for (const obj of objects) {
207
+ const tableName = obj.name;
208
+
209
+ // Cache JSON fields
210
+ const jsonCols: string[] = [];
211
+ if (obj.fields) {
212
+ for (const [name, field] of Object.entries<any>(obj.fields)) {
213
+ const type = field.type || 'string';
214
+ if (this.isJsonField(type, field)) {
215
+ jsonCols.push(name);
216
+ }
217
+ }
218
+ }
219
+ this.jsonFields[tableName] = jsonCols;
220
+
221
+ let exists = await this.knex.schema.hasTable(tableName);
222
+
223
+ if (exists) {
224
+ const columnInfo = await this.knex(tableName).columnInfo();
225
+ const existingColumns = Object.keys(columnInfo);
226
+
227
+ // Check for _id vs id conflict (Legacy _id from mongo-style init)
228
+ if (existingColumns.includes('_id') && !existingColumns.includes('id')) {
229
+ console.log(`[KnexDriver] Detected legacy '_id' in '${tableName}'. Recreating table for 'id' compatibility...`);
230
+ await this.knex.schema.dropTable(tableName);
231
+ exists = false;
232
+ }
233
+ }
234
+
235
+ if (!exists) {
236
+ await this.knex.schema.createTable(tableName, (table) => {
237
+ // Use standard 'id' for SQL databases
238
+ table.string('id').primary();
239
+ table.timestamp('created_at').defaultTo(this.knex.fn.now());
240
+ table.timestamp('updated_at').defaultTo(this.knex.fn.now());
241
+ if (obj.fields) {
242
+ for (const [name, field] of Object.entries(obj.fields)) {
243
+ this.createColumn(table, name, field);
244
+ }
245
+ }
246
+ });
247
+ console.log(`[KnexDriver] Created table '${tableName}'`);
248
+ } else {
249
+ const columnInfo = await this.knex(tableName).columnInfo();
250
+ const existingColumns = Object.keys(columnInfo);
251
+
252
+ await this.knex.schema.alterTable(tableName, (table) => {
253
+ if (obj.fields) {
254
+ for (const [name, field] of Object.entries(obj.fields)) {
255
+ if (!existingColumns.includes(name)) {
256
+ this.createColumn(table, name, field);
257
+ console.log(`[KnexDriver] Added column '${name}' to '${tableName}'`);
258
+ }
259
+ }
260
+ }
261
+ });
262
+ }
263
+ }
264
+ }
265
+
266
+ private createColumn(table: Knex.CreateTableBuilder, name: string, field: any) {
267
+ if (field.multiple) {
268
+ table.json(name);
269
+ return;
270
+ }
271
+
272
+ const type = field.type || 'string';
273
+ let col;
274
+ switch(type) {
275
+ case 'string':
276
+ case 'email':
277
+ case 'url':
278
+ case 'phone':
279
+ case 'password':
280
+ col = table.string(name); break;
281
+ case 'text':
282
+ case 'textarea':
283
+ case 'html':
284
+ case 'markdown':
285
+ col = table.text(name); break;
286
+ case 'integer':
287
+ case 'int': col = table.integer(name); break;
288
+ case 'float':
289
+ case 'number':
290
+ case 'currency':
291
+ case 'percent': col = table.float(name); break;
292
+ case 'boolean': col = table.boolean(name); break;
293
+ case 'date': col = table.date(name); break;
294
+ case 'datetime': col = table.timestamp(name); break;
295
+ case 'time': col = table.time(name); break;
296
+ case 'json':
297
+ case 'object':
298
+ case 'array':
299
+ case 'image':
300
+ case 'file':
301
+ case 'avatar':
302
+ case 'location': col = table.json(name); break;
303
+ case 'summary': col = table.float(name); break; // Stored calculation result
304
+ case 'auto_number': col = table.string(name); break; // Generated string
305
+ case 'formula': return; // Virtual field, do not create column
306
+ default: col = table.string(name);
307
+ }
308
+
309
+ if (field.unique) {
310
+ col.unique();
311
+ }
312
+ if (field.required) {
313
+ col.notNullable();
314
+ }
315
+ }
316
+
317
+ private async ensureDatabaseExists() {
318
+ if (this.config.client !== 'pg' && this.config.client !== 'postgresql') return;
319
+
320
+ try {
321
+ await this.knex.raw('SELECT 1');
322
+ } catch (e: any) {
323
+ if (e.code === '3D000') { // Database does not exist
324
+ await this.createDatabase();
325
+ } else {
326
+ throw e;
327
+ }
328
+ }
329
+ }
330
+
331
+ private async createDatabase() {
332
+ const config = this.config;
333
+ const connection = config.connection;
334
+ let dbName = '';
335
+ let adminConfig = { ...config };
336
+
337
+ if (typeof connection === 'string') {
338
+ const url = new URL(connection);
339
+ dbName = url.pathname.slice(1);
340
+ url.pathname = '/postgres';
341
+ adminConfig.connection = url.toString();
342
+ } else {
343
+ dbName = connection.database;
344
+ adminConfig.connection = { ...connection, database: 'postgres' };
345
+ }
346
+
347
+ console.log(`[KnexDriver] Database '${dbName}' does not exist. Creating...`);
348
+
349
+ const adminKnex = knex(adminConfig);
350
+ try {
351
+ await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
352
+ console.log(`[KnexDriver] Database '${dbName}' created successfully.`);
353
+ } catch (e: any) {
354
+ console.error(`[KnexDriver] Failed to create database '${dbName}':`, e.message);
355
+ if (e.code === '42501') {
356
+ console.error(`[KnexDriver] Hint: The user '${adminConfig.connection.user || 'current user'}' does not have CREATEDB privileges.`);
357
+ console.error(`[KnexDriver] Please run: createdb ${dbName}`);
358
+ }
359
+ throw e;
360
+ } finally {
361
+ await adminKnex.destroy();
362
+ }
363
+ }
364
+
365
+ private isJsonField(type: string, field: any) {
366
+ return ['json', 'object', 'array', 'image', 'file', 'avatar', 'location'].includes(type) || field.multiple;
367
+ }
368
+
369
+ private formatInput(objectName: string, data: any) {
370
+ // Only needed for SQLite usually, PG handles JSON
371
+ const isSqlite = this.config.client === 'sqlite3';
372
+ if (!isSqlite) return data;
373
+
374
+ const fields = this.jsonFields[objectName];
375
+ if (!fields || fields.length === 0) return data;
376
+
377
+ const copy = { ...data };
378
+ for (const field of fields) {
379
+ if (copy[field] !== undefined && typeof copy[field] === 'object' && copy[field] !== null) {
380
+ copy[field] = JSON.stringify(copy[field]);
381
+ }
382
+ }
383
+ return copy;
384
+ }
385
+
386
+ private formatOutput(objectName: string, data: any) {
387
+ const isSqlite = this.config.client === 'sqlite3';
388
+ if (!isSqlite) return data;
389
+
390
+ const fields = this.jsonFields[objectName];
391
+ if (!fields || fields.length === 0) return data;
392
+
393
+ // data is a single row object
394
+ for (const field of fields) {
395
+ if (data[field] !== undefined && typeof data[field] === 'string') {
396
+ try {
397
+ data[field] = JSON.parse(data[field]);
398
+ } catch (e) {
399
+ // ignore parse error, keep as string
400
+ }
401
+ }
402
+ }
403
+ return data;
404
+ }
170
405
  }
171
406
 
@@ -1,5 +1,5 @@
1
1
  import { KnexDriver } from '../src';
2
- import { UnifiedQuery } from '@objectql/core';
2
+ import { UnifiedQuery } from '@objectql/types';
3
3
 
4
4
  describe('KnexDriver (SQLite Integration)', () => {
5
5
  let driver: KnexDriver;
@@ -17,16 +17,16 @@ describe('KnexDriver (SQLite Integration)', () => {
17
17
  const k = (driver as any).knex;
18
18
 
19
19
  await k.schema.createTable('users', (t: any) => {
20
- t.increments('id');
20
+ t.string('id').primary();
21
21
  t.string('name');
22
22
  t.integer('age');
23
23
  });
24
24
 
25
25
  await k('users').insert([
26
- { name: 'Alice', age: 25 },
27
- { name: 'Bob', age: 17 },
28
- { name: 'Charlie', age: 30 },
29
- { name: 'Dave', age: 17 }
26
+ { id: '1', name: 'Alice', age: 25 },
27
+ { id: '2', name: 'Bob', age: 17 },
28
+ { id: '3', name: 'Charlie', age: 30 },
29
+ { id: '4', name: 'Dave', age: 17 }
30
30
  ]);
31
31
  });
32
32
 
@@ -105,5 +105,16 @@ describe('KnexDriver (SQLite Integration)', () => {
105
105
  const count = await driver.count('users', [['age', '=', 17]]);
106
106
  expect(count).toBe(2);
107
107
  });
108
+
109
+ it('should map _id to id if provided', async () => {
110
+ const newItemWithId = { _id: 'custom-id', name: 'Frank', age: 40 };
111
+ const created = await driver.create('users', newItemWithId);
112
+
113
+ expect(created.id).toBe('custom-id');
114
+ // Check if we can retrieve it by id
115
+ const fetched = await driver.findOne('users', 'custom-id');
116
+ expect(fetched).toBeDefined();
117
+ expect(fetched.name).toBe('Frank');
118
+ });
108
119
  });
109
120
 
@@ -0,0 +1,253 @@
1
+ import { KnexDriver } from '../src';
2
+
3
+ describe('KnexDriver Schema Sync (SQLite)', () => {
4
+ let driver: KnexDriver;
5
+ let knexInstance: any;
6
+
7
+ beforeEach(async () => {
8
+ // Init ephemeral in-memory database
9
+ driver = new KnexDriver({
10
+ client: 'sqlite3',
11
+ connection: {
12
+ filename: ':memory:'
13
+ },
14
+ useNullAsDefault: true
15
+ });
16
+ knexInstance = (driver as any).knex;
17
+ });
18
+
19
+ afterEach(async () => {
20
+ await knexInstance.destroy();
21
+ });
22
+
23
+ it('should create table if not exists', async () => {
24
+ const objects = [{
25
+ name: 'test_obj',
26
+ fields: {
27
+ name: { type: 'string' },
28
+ age: { type: 'integer' }
29
+ }
30
+ }];
31
+
32
+ await driver.init(objects);
33
+
34
+ const exists = await knexInstance.schema.hasTable('test_obj');
35
+ expect(exists).toBe(true);
36
+
37
+ const columns = await knexInstance('test_obj').columnInfo();
38
+ expect(columns).toHaveProperty('id');
39
+ expect(columns).toHaveProperty('created_at');
40
+ expect(columns).toHaveProperty('updated_at');
41
+ expect(columns).toHaveProperty('name');
42
+ expect(columns).toHaveProperty('age');
43
+ });
44
+
45
+ it('should add new columns if table exists', async () => {
46
+ // 1. Setup existing table with subset of columns
47
+ await knexInstance.schema.createTable('test_obj', (t: any) => {
48
+ t.string('id').primary();
49
+ t.string('name');
50
+ });
51
+
52
+ // 2. Insert some data
53
+ await knexInstance('test_obj').insert({ id: '1', name: 'Old Data' });
54
+
55
+ // 3. Init with new fields
56
+ const objects = [{
57
+ name: 'test_obj',
58
+ fields: {
59
+ name: { type: 'string' },
60
+ age: { type: 'integer' }, // New field
61
+ active: { type: 'boolean' } // New field
62
+ }
63
+ }];
64
+
65
+ await driver.init(objects);
66
+
67
+ // 4. Verify columns
68
+ const columns = await knexInstance('test_obj').columnInfo();
69
+ expect(columns).toHaveProperty('age');
70
+ expect(columns).toHaveProperty('active');
71
+
72
+ // 5. Verify data is intact
73
+ const row = await knexInstance('test_obj').where('id', '1').first();
74
+ expect(row.name).toBe('Old Data');
75
+ });
76
+
77
+ it('should not delete existing columns', async () => {
78
+ // 1. Setup table with extra column
79
+ await knexInstance.schema.createTable('test_obj', (t: any) => {
80
+ t.string('id').primary();
81
+ t.string('name');
82
+ t.string('extra_column'); // Should stay
83
+ });
84
+
85
+ // 2. Init with only 'name'
86
+ const objects = [{
87
+ name: 'test_obj',
88
+ fields: {
89
+ name: { type: 'string' }
90
+ }
91
+ }];
92
+
93
+ await driver.init(objects);
94
+
95
+ const columns = await knexInstance('test_obj').columnInfo();
96
+ expect(columns).toHaveProperty('name');
97
+ expect(columns).toHaveProperty('extra_column'); // Preservation check
98
+ });
99
+
100
+ it('should not fail if table creation is repeated', async () => {
101
+ const objects = [{
102
+ name: 'test_obj',
103
+ fields: {
104
+ name: { type: 'string' }
105
+ }
106
+ }];
107
+
108
+ // First init
109
+ await driver.init(objects);
110
+
111
+ // Second init (should be idempotent-ish, or just skip creation)
112
+ await driver.init(objects);
113
+
114
+ const exists = await knexInstance.schema.hasTable('test_obj');
115
+ expect(exists).toBe(true);
116
+ });
117
+
118
+ it('should create json column for multiple=true fields', async () => {
119
+ const objects = [{
120
+ name: 'multi_test',
121
+ fields: {
122
+ tags: { type: 'select', multiple: true } as any,
123
+ users: { type: 'lookup', reference_to: 'user', multiple: true } as any
124
+ }
125
+ }];
126
+
127
+ await driver.init(objects);
128
+
129
+ const columns = await knexInstance('multi_test').columnInfo();
130
+ // Types in SQLite might be generic, but verifying we can insert/read array is best.
131
+
132
+ // Try inserting array data
133
+ await driver.create('multi_test', {
134
+ tags: ['a', 'b'],
135
+ users: ['u1', 'u2']
136
+ });
137
+
138
+ const results = await driver.find('multi_test', {});
139
+ const row = results[0];
140
+
141
+ // Driver should automatically parse JSON columns for SQLite
142
+ expect(row.tags).toEqual(['a', 'b']);
143
+ expect(row.users).toEqual(['u1', 'u2']);
144
+ });
145
+
146
+ it('should create percent column', async () => {
147
+ const objects = [{
148
+ name: 'percent_test',
149
+ fields: {
150
+ completion: { type: 'percent' } as any
151
+ }
152
+ }];
153
+
154
+ await driver.init(objects);
155
+
156
+ const columns = await knexInstance('percent_test').columnInfo();
157
+ expect(columns).toHaveProperty('completion');
158
+
159
+ // Insert a percentage
160
+ await driver.create('percent_test', { completion: 0.85 });
161
+ const res = await driver.find('percent_test', {});
162
+ expect(res[0].completion).toBe(0.85);
163
+ });
164
+
165
+ it('should handle special fields (formula, summary, auto_number)', async () => {
166
+ const objects = [{
167
+ name: 'special_fields_test',
168
+ fields: {
169
+ // Formula should NOT create a column
170
+ total: { type: 'formula', expression: 'price * qty', data_type: 'number' } as any,
171
+ // Summary should create a numeric column
172
+ child_count: { type: 'summary', summary_object: 'child_obj', summary_type: 'count' } as any,
173
+ // Auto Number should create a string column
174
+ invoice_no: { type: 'auto_number', auto_number_format: 'INV-{0000}' } as any
175
+ }
176
+ }];
177
+
178
+ await driver.init(objects);
179
+
180
+ const columns = await knexInstance('special_fields_test').columnInfo();
181
+
182
+ expect(columns).not.toHaveProperty('total');
183
+ expect(columns).toHaveProperty('child_count');
184
+ expect(columns).toHaveProperty('invoice_no');
185
+ });
186
+
187
+ it('should create database constraints (unique, required)', async () => {
188
+ const objects = [{
189
+ name: 'constraint_test',
190
+ fields: {
191
+ unique_field: { type: 'string', unique: true } as any,
192
+ required_field: { type: 'string', required: true } as any
193
+ }
194
+ }];
195
+
196
+ await driver.init(objects);
197
+
198
+ // Verify Unique using negative test?
199
+ // SQLite enforces unique.
200
+ await driver.create('constraint_test', { unique_field: 'u1', required_field: 'r1' });
201
+
202
+ try {
203
+ await driver.create('constraint_test', { unique_field: 'u1', required_field: 'r2' });
204
+ fail('Should throw error for unique violation');
205
+ } catch (e: any) {
206
+ expect(e.message).toMatch(/UNIQUE constraint failed|duplicate key value/);
207
+ }
208
+
209
+ try {
210
+ await driver.create('constraint_test', { unique_field: 'u2' });
211
+ fail('Should throw error for not null violation');
212
+ } catch (e: any) {
213
+ expect(e.message).toMatch(/NOT NULL constraint failed|null value in column/);
214
+ }
215
+ });
216
+
217
+ it('should handle new field types (email, file, location)', async () => {
218
+ const objects = [{
219
+ name: 'new_types_test',
220
+ fields: {
221
+ email: { type: 'email' } as any,
222
+ profile_pic: { type: 'image' } as any,
223
+ resume: { type: 'file' } as any,
224
+ office_loc: { type: 'location' } as any,
225
+ work_hours: { type: 'time' } as any
226
+ }
227
+ }];
228
+
229
+ await driver.init(objects);
230
+ const cols = await knexInstance('new_types_test').columnInfo();
231
+
232
+ expect(cols).toHaveProperty('email');
233
+ // File/Image/Location/Avatar are stored as JSON in KnexDriver mapping
234
+ // But columnInfo might report 'text' for sqlite or 'json' for postgres.
235
+ // We just ensure they were created.
236
+ expect(cols).toHaveProperty('profile_pic');
237
+ expect(cols).toHaveProperty('resume');
238
+
239
+ // Test Insert for complex types
240
+ await driver.create('new_types_test', {
241
+ email: 'test@example.com',
242
+ profile_pic: { url: 'http://img.com/1.png' },
243
+ office_loc: { lat: 10, lng: 20 },
244
+ work_hours: '09:00:00'
245
+ });
246
+
247
+ const res = await driver.find('new_types_test', {});
248
+ const row = res[0];
249
+
250
+ expect(row.email).toBe('test@example.com');
251
+ expect(row.office_loc).toEqual({ lat: 10, lng: 20 }); // Auto-parsed by driver logic
252
+ });
253
+ });