@objectstack/driver-sql 4.0.4 → 4.1.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.
@@ -1,243 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
4
- import { SqlDriver } from '../src/index.js';
5
-
6
- /**
7
- * QueryAST format tests — verifies compatibility with @objectstack/spec QueryAST.
8
- */
9
- describe('SqlDriver (QueryAST Format)', () => {
10
- let driver: SqlDriver;
11
-
12
- beforeEach(async () => {
13
- driver = new SqlDriver({
14
- client: 'better-sqlite3',
15
- connection: { filename: ':memory:' },
16
- useNullAsDefault: true,
17
- });
18
-
19
- const k = (driver as any).knex;
20
-
21
- await k.schema.createTable('products', (t: any) => {
22
- t.string('id').primary();
23
- t.string('name');
24
- t.float('price');
25
- t.string('category');
26
- });
27
-
28
- await k('products').insert([
29
- { id: '1', name: 'Laptop', price: 1200, category: 'Electronics' },
30
- { id: '2', name: 'Mouse', price: 25, category: 'Electronics' },
31
- { id: '3', name: 'Desk', price: 350, category: 'Furniture' },
32
- { id: '4', name: 'Chair', price: 200, category: 'Furniture' },
33
- { id: '5', name: 'Monitor', price: 400, category: 'Electronics' },
34
- ]);
35
- });
36
-
37
- afterEach(async () => {
38
- await driver.disconnect();
39
- });
40
-
41
- describe('Driver Metadata', () => {
42
- it('should expose driver metadata for ObjectStack compatibility', () => {
43
- expect(driver.name).toBe('com.objectstack.driver.sql');
44
- expect(driver.version).toBeDefined();
45
- expect(driver.supports).toBeDefined();
46
- expect(driver.supports.transactions).toBe(true);
47
- expect(driver.supports.joins).toBe(true);
48
- });
49
- });
50
-
51
- describe('Lifecycle Methods', () => {
52
- it('should support connect method', async () => {
53
- await expect(driver.connect()).resolves.toBeUndefined();
54
- });
55
-
56
- it('should support checkHealth method', async () => {
57
- const healthy = await driver.checkHealth();
58
- expect(healthy).toBe(true);
59
- });
60
-
61
- it('should support disconnect method', async () => {
62
- const testDriver = new SqlDriver({
63
- client: 'better-sqlite3',
64
- connection: { filename: ':memory:' },
65
- useNullAsDefault: true,
66
- });
67
-
68
- await expect(testDriver.disconnect()).resolves.toBeUndefined();
69
- const healthy = await testDriver.checkHealth();
70
- expect(healthy).toBe(false);
71
- });
72
- });
73
-
74
- describe('QueryAST Format Support', () => {
75
- it('should support QueryAST with limit and orderBy', async () => {
76
- const results = await driver.find('products', {
77
- fields: ['name', 'price'],
78
- limit: 2,
79
- orderBy: [{ field: 'price', order: 'asc' as const }],
80
- } as any);
81
-
82
- expect(results.length).toBe(2);
83
- expect(results[0].name).toBe('Mouse');
84
- expect(results[1].name).toBe('Chair');
85
- });
86
-
87
- it('should support QueryAST orderBy with object notation', async () => {
88
- const results = await driver.find('products', {
89
- fields: ['name'],
90
- orderBy: [
91
- { field: 'category', order: 'asc' as const },
92
- { field: 'price', order: 'desc' as const },
93
- ],
94
- } as any);
95
-
96
- expect(results.length).toBe(5);
97
- expect(results[0].name).toBe('Laptop');
98
- expect(results[3].name).toBe('Desk');
99
- });
100
-
101
- it('should support QueryAST with where, offset, limit, and orderBy', async () => {
102
- const results = await driver.find('products', {
103
- where: [['category', '=', 'Electronics']],
104
- offset: 1,
105
- limit: 1,
106
- orderBy: [{ field: 'price', order: 'asc' as const }],
107
- } as any);
108
-
109
- expect(results.length).toBe(1);
110
- expect(results[0].name).toBe('Monitor');
111
- });
112
-
113
- it('should support aggregations in QueryAST format', async () => {
114
- const results = await driver.aggregate('products', {
115
- aggregations: [
116
- { function: 'sum' as const, field: 'price', alias: 'total_price' },
117
- { function: 'count' as const, field: '*', alias: 'count' },
118
- ],
119
- groupBy: ['category'],
120
- });
121
-
122
- expect(results.length).toBe(2);
123
-
124
- const electronics = results.find((r: any) => r.category === 'Electronics');
125
- const furniture = results.find((r: any) => r.category === 'Furniture');
126
-
127
- expect(electronics).toBeDefined();
128
- expect(electronics.total_price).toBe(1625);
129
-
130
- expect(furniture).toBeDefined();
131
- expect(furniture.total_price).toBe(550);
132
- });
133
-
134
- it('should support count with QueryAST where clause', async () => {
135
- const count = await driver.count('products', {
136
- where: [['price', '>', 300]],
137
- } as any);
138
- expect(count).toBe(3);
139
- });
140
- });
141
-
142
- describe('Standard QueryAST Pagination', () => {
143
- it('should support limit with orderBy using standard keys', async () => {
144
- const results = await driver.find('products', {
145
- fields: ['name'],
146
- limit: 2,
147
- orderBy: [{ field: 'price', order: 'asc' }],
148
- } as any);
149
-
150
- expect(results.length).toBe(2);
151
- expect(results[0].name).toBe('Mouse');
152
- });
153
-
154
- it('should still support legacy aggregate format', async () => {
155
- const results = await driver.aggregate('products', {
156
- aggregate: [{ func: 'avg', field: 'price', alias: 'avg_price' }],
157
- groupBy: ['category'],
158
- });
159
-
160
- expect(results.length).toBe(2);
161
- const electronics = results.find((r: any) => r.category === 'Electronics');
162
- expect(electronics.avg_price).toBeCloseTo(541.67, 1);
163
- });
164
-
165
- it('should support offset and limit with orderBy', async () => {
166
- const results = await driver.find('products', {
167
- limit: 3,
168
- offset: 2,
169
- orderBy: [{ field: 'name', order: 'asc' as const }],
170
- } as any);
171
-
172
- expect(results.length).toBe(3);
173
- expect(results[0].name).toBe('Laptop');
174
- expect(results[1].name).toBe('Monitor');
175
- expect(results[2].name).toBe('Mouse');
176
- });
177
- });
178
-
179
- describe('Legacy Keys Are Ignored', () => {
180
- it('should ignore legacy "filters" key — only "where" is recognized', async () => {
181
- const results = await driver.find('products', {
182
- filters: [['category', '=', 'Furniture']],
183
- } as any);
184
-
185
- // "filters" is not recognized, so no WHERE clause is applied — returns all rows
186
- expect(results.length).toBe(5);
187
- });
188
-
189
- it('should use "where" and ignore "filters" when both are present', async () => {
190
- const results = await driver.find('products', {
191
- where: [['category', '=', 'Electronics']],
192
- filters: [['category', '=', 'Furniture']],
193
- } as any);
194
-
195
- // Only "where" is applied — returns Electronics, not Furniture
196
- expect(results.every((r: any) => r.category === 'Electronics')).toBe(true);
197
- expect(results.length).toBe(3);
198
- });
199
-
200
- it('should ignore legacy "sort" key — only "orderBy" is recognized', async () => {
201
- const results = await driver.find('products', {
202
- fields: ['name'],
203
- limit: 5,
204
- orderBy: [{ field: 'price', order: 'desc' as const }],
205
- sort: [{ field: 'price', order: 'asc' as const }],
206
- } as any);
207
-
208
- // "sort" is ignored; "orderBy" (desc) is applied — most expensive first
209
- expect(results.length).toBe(5);
210
- expect(results[0].name).toBe('Laptop');
211
- expect(results[4].name).toBe('Mouse');
212
- });
213
-
214
- it('should ignore legacy "skip" key — only "offset" is recognized', async () => {
215
- const results = await driver.find('products', {
216
- skip: 3,
217
- orderBy: [{ field: 'name', order: 'asc' as const }],
218
- } as any);
219
-
220
- // "skip" is not recognized — no offset applied, returns all 5 rows
221
- expect(results.length).toBe(5);
222
- });
223
-
224
- it('should ignore legacy "top" key — only "limit" is recognized', async () => {
225
- const results = await driver.find('products', {
226
- top: 2,
227
- orderBy: [{ field: 'name', order: 'asc' as const }],
228
- } as any);
229
-
230
- // "top" is not recognized — no limit applied, returns all 5 rows
231
- expect(results.length).toBe(5);
232
- });
233
-
234
- it('should ignore legacy "filters" in count — only "where" is recognized', async () => {
235
- const count = await driver.count('products', {
236
- filters: [['price', '>', 300]],
237
- } as any);
238
-
239
- // "filters" is not recognized — counts all rows
240
- expect(count).toBe(5);
241
- });
242
- });
243
- });
@@ -1,313 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
4
- import { SqlDriver } from '../src/index.js';
5
-
6
- describe('SqlDriver Schema Sync (SQLite)', () => {
7
- let driver: SqlDriver;
8
- let knexInstance: any;
9
-
10
- beforeEach(async () => {
11
- driver = new SqlDriver({
12
- client: 'better-sqlite3',
13
- connection: { filename: ':memory:' },
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
- {
26
- name: 'test_obj',
27
- fields: {
28
- name: { type: 'string' },
29
- age: { type: 'integer' },
30
- },
31
- },
32
- ];
33
-
34
- await driver.initObjects(objects);
35
-
36
- const exists = await knexInstance.schema.hasTable('test_obj');
37
- expect(exists).toBe(true);
38
-
39
- const columns = await knexInstance('test_obj').columnInfo();
40
- expect(columns).toHaveProperty('id');
41
- expect(columns).toHaveProperty('created_at');
42
- expect(columns).toHaveProperty('updated_at');
43
- expect(columns).toHaveProperty('name');
44
- expect(columns).toHaveProperty('age');
45
- });
46
-
47
- it('should add new columns if table exists', async () => {
48
- await knexInstance.schema.createTable('test_obj', (t: any) => {
49
- t.string('id').primary();
50
- t.string('name');
51
- });
52
-
53
- await knexInstance('test_obj').insert({ id: '1', name: 'Old Data' });
54
-
55
- const objects = [
56
- {
57
- name: 'test_obj',
58
- fields: {
59
- name: { type: 'string' },
60
- age: { type: 'integer' },
61
- active: { type: 'boolean' },
62
- },
63
- },
64
- ];
65
-
66
- await driver.initObjects(objects);
67
-
68
- const columns = await knexInstance('test_obj').columnInfo();
69
- expect(columns).toHaveProperty('age');
70
- expect(columns).toHaveProperty('active');
71
-
72
- const row = await knexInstance('test_obj').where('id', '1').first();
73
- expect(row.name).toBe('Old Data');
74
- });
75
-
76
- it('should not delete existing columns', async () => {
77
- await knexInstance.schema.createTable('test_obj', (t: any) => {
78
- t.string('id').primary();
79
- t.string('name');
80
- t.string('extra_column');
81
- });
82
-
83
- const objects = [
84
- {
85
- name: 'test_obj',
86
- fields: {
87
- name: { type: 'string' },
88
- },
89
- },
90
- ];
91
-
92
- await driver.initObjects(objects);
93
-
94
- const columns = await knexInstance('test_obj').columnInfo();
95
- expect(columns).toHaveProperty('name');
96
- expect(columns).toHaveProperty('extra_column');
97
- });
98
-
99
- it('should not fail if table creation is repeated', async () => {
100
- const objects = [
101
- {
102
- name: 'test_obj',
103
- fields: {
104
- name: { type: 'string' },
105
- },
106
- },
107
- ];
108
-
109
- await driver.initObjects(objects);
110
- await driver.initObjects(objects);
111
-
112
- const exists = await knexInstance.schema.hasTable('test_obj');
113
- expect(exists).toBe(true);
114
- });
115
-
116
- it('should create json column for multiple=true fields', async () => {
117
- const objects = [
118
- {
119
- name: 'multi_test',
120
- fields: {
121
- tags: { type: 'select', multiple: true } as any,
122
- users: { type: 'lookup', reference_to: 'user', multiple: true } as any,
123
- },
124
- },
125
- ];
126
-
127
- await driver.initObjects(objects);
128
-
129
- await driver.create('multi_test', {
130
- tags: ['a', 'b'],
131
- users: ['u1', 'u2'],
132
- });
133
-
134
- const results = await driver.find('multi_test', {});
135
- const row = results[0];
136
-
137
- expect(row.tags).toEqual(['a', 'b']);
138
- expect(row.users).toEqual(['u1', 'u2']);
139
- });
140
-
141
- it('should create percent column', async () => {
142
- const objects = [
143
- {
144
- name: 'percent_test',
145
- fields: {
146
- completion: { type: 'percent' } as any,
147
- },
148
- },
149
- ];
150
-
151
- await driver.initObjects(objects);
152
-
153
- const columns = await knexInstance('percent_test').columnInfo();
154
- expect(columns).toHaveProperty('completion');
155
-
156
- await driver.create('percent_test', { completion: 0.85 });
157
- const res = await driver.find('percent_test', {});
158
- expect(res[0].completion).toBe(0.85);
159
- });
160
-
161
- it('should handle special fields (formula, summary, auto_number)', async () => {
162
- const objects = [
163
- {
164
- name: 'special_fields_test',
165
- fields: {
166
- total: { type: 'formula', expression: 'price * qty', data_type: 'number' } as any,
167
- child_count: { type: 'summary', summary_object: 'child_obj', summary_type: 'count' } as any,
168
- invoice_no: { type: 'auto_number', auto_number_format: 'INV-{0000}' } as any,
169
- },
170
- },
171
- ];
172
-
173
- await driver.initObjects(objects);
174
-
175
- const columns = await knexInstance('special_fields_test').columnInfo();
176
-
177
- expect(columns).not.toHaveProperty('total');
178
- expect(columns).toHaveProperty('child_count');
179
- expect(columns).toHaveProperty('invoice_no');
180
- });
181
-
182
- it('should create database constraints (unique, required)', async () => {
183
- const objects = [
184
- {
185
- name: 'constraint_test',
186
- fields: {
187
- unique_field: { type: 'string', unique: true } as any,
188
- required_field: { type: 'string', required: true } as any,
189
- },
190
- },
191
- ];
192
-
193
- await driver.initObjects(objects);
194
-
195
- await driver.create('constraint_test', { unique_field: 'u1', required_field: 'r1' });
196
-
197
- try {
198
- await driver.create('constraint_test', { unique_field: 'u1', required_field: 'r2' });
199
- expect.unreachable('Should throw error for unique violation');
200
- } catch (e: any) {
201
- expect(e.message).toMatch(/UNIQUE constraint failed|duplicate key value/);
202
- }
203
-
204
- try {
205
- await driver.create('constraint_test', { unique_field: 'u2' });
206
- expect.unreachable('Should throw error for not null violation');
207
- } catch (e: any) {
208
- expect(e.message).toMatch(/NOT NULL constraint failed|null value in column/);
209
- }
210
- });
211
-
212
- it('should handle new field types (email, file, location)', async () => {
213
- const objects = [
214
- {
215
- name: 'new_types_test',
216
- fields: {
217
- email: { type: 'email' } as any,
218
- profile_pic: { type: 'image' } as any,
219
- resume: { type: 'file' } as any,
220
- office_loc: { type: 'location' } as any,
221
- work_hours: { type: 'time' } as any,
222
- },
223
- },
224
- ];
225
-
226
- await driver.initObjects(objects);
227
- const cols = await knexInstance('new_types_test').columnInfo();
228
-
229
- expect(cols).toHaveProperty('email');
230
- expect(cols).toHaveProperty('profile_pic');
231
- expect(cols).toHaveProperty('resume');
232
-
233
- await driver.create('new_types_test', {
234
- email: 'test@example.com',
235
- profile_pic: { url: 'http://img.com/1.png' },
236
- office_loc: { lat: 10, lng: 20 },
237
- work_hours: '09:00:00',
238
- });
239
-
240
- const res = await driver.find('new_types_test', {});
241
- const row = res[0];
242
-
243
- expect(row.email).toBe('test@example.com');
244
- expect(row.office_loc).toEqual({ lat: 10, lng: 20 });
245
- });
246
-
247
- it('should skip ensureDatabaseExists for SQLite (no-op)', async () => {
248
- // SQLite auto-creates database files, so ensureDatabaseExists should be a no-op
249
- // This test verifies that initObjects works normally for SQLite without errors
250
- const objects = [
251
- {
252
- name: 'db_check_test',
253
- fields: {
254
- value: { type: 'string' },
255
- },
256
- },
257
- ];
258
-
259
- // Should not throw — SQLite skips ensureDatabaseExists
260
- await driver.initObjects(objects);
261
-
262
- const exists = await knexInstance.schema.hasTable('db_check_test');
263
- expect(exists).toBe(true);
264
- });
265
-
266
- it('should use object parameter (physical table name) in syncSchema, not schema.name (FQN)', async () => {
267
- // Simulates the real-world scenario: syncRegisteredSchemas passes the
268
- // physical table name 'sys_user' while the schema object has name 'sys__user' (FQN).
269
- const physicalTableName = 'sys_user';
270
- const fqnName = 'sys__user';
271
-
272
- await driver.syncSchema(physicalTableName, {
273
- name: fqnName,
274
- fields: {
275
- email: { type: 'string' },
276
- display_name: { type: 'string' },
277
- },
278
- });
279
-
280
- // The table should be created with the physical name, not the FQN
281
- const existsPhysical = await knexInstance.schema.hasTable(physicalTableName);
282
- expect(existsPhysical).toBe(true);
283
-
284
- const existsFqn = await knexInstance.schema.hasTable(fqnName);
285
- expect(existsFqn).toBe(false);
286
-
287
- // Verify the table has the correct columns
288
- const columns = await knexInstance(physicalTableName).columnInfo();
289
- expect(columns).toHaveProperty('id');
290
- expect(columns).toHaveProperty('email');
291
- expect(columns).toHaveProperty('display_name');
292
- });
293
-
294
- it('should prefer tableName over name in initObjects for defense-in-depth', async () => {
295
- const objects = [
296
- {
297
- name: 'crm__deal',
298
- tableName: 'crm_deal',
299
- fields: {
300
- amount: { type: 'number' },
301
- },
302
- },
303
- ];
304
-
305
- await driver.initObjects(objects);
306
-
307
- const existsPhysical = await knexInstance.schema.hasTable('crm_deal');
308
- expect(existsPhysical).toBe(true);
309
-
310
- const existsFqn = await knexInstance.schema.hasTable('crm__deal');
311
- expect(existsFqn).toBe(false);
312
- });
313
- });
@@ -1,108 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
4
- import { SqlDriver } from '../src/index.js';
5
-
6
- describe('SqlDriver (SQLite Integration)', () => {
7
- let driver: SqlDriver;
8
-
9
- beforeEach(async () => {
10
- driver = new SqlDriver({
11
- client: 'better-sqlite3',
12
- connection: { filename: ':memory:' },
13
- useNullAsDefault: true,
14
- });
15
-
16
- const k = (driver as any).knex;
17
-
18
- await k.schema.createTable('users', (t: any) => {
19
- t.string('id').primary();
20
- t.string('name');
21
- t.integer('age');
22
- });
23
-
24
- await k('users').insert([
25
- { id: '1', name: 'Alice', age: 25 },
26
- { id: '2', name: 'Bob', age: 17 },
27
- { id: '3', name: 'Charlie', age: 30 },
28
- { id: '4', name: 'Dave', age: 17 },
29
- ]);
30
- });
31
-
32
- afterEach(async () => {
33
- await driver.disconnect();
34
- });
35
-
36
- it('should be instantiable', () => {
37
- expect(driver).toBeDefined();
38
- expect(driver).toBeInstanceOf(SqlDriver);
39
- });
40
-
41
- it('should find objects with filters', async () => {
42
- const results = await driver.find('users', {
43
- fields: ['name', 'age'],
44
- where: { age: { $gt: 18 } },
45
- orderBy: [{ field: 'name', order: 'asc' }],
46
- });
47
-
48
- expect(results.length).toBe(2);
49
- expect(results.map((r: any) => r.name)).toEqual(['Alice', 'Charlie']);
50
- });
51
-
52
- it('should apply simple AND/OR logic', async () => {
53
- const results = await driver.find('users', {
54
- where: {
55
- $or: [{ age: 17 }, { age: { $gt: 29 } }],
56
- },
57
- });
58
- const names = results.map((r: any) => r.name).sort();
59
- expect(names).toEqual(['Bob', 'Charlie', 'Dave']);
60
- });
61
-
62
- it('should find one object by id', async () => {
63
- const [alice] = await driver.find('users', { where: { name: 'Alice' } });
64
- expect(alice).toBeDefined();
65
-
66
- const fetched = await driver.findOne('users', alice.id as any);
67
- expect(fetched).toBeDefined();
68
- expect(fetched.name).toBe('Alice');
69
- });
70
-
71
- it('should create an object', async () => {
72
- await driver.create('users', { name: 'Eve', age: 22 });
73
-
74
- const [eve] = await driver.find('users', { where: { name: 'Eve' } });
75
- expect(eve).toBeDefined();
76
- expect(eve.age).toBe(22);
77
- });
78
-
79
- it('should update an object', async () => {
80
- const [bob] = await driver.find('users', { where: { name: 'Bob' } });
81
- await driver.update('users', bob.id, { age: 18 });
82
-
83
- const updated = await driver.findOne('users', bob.id as any);
84
- expect(updated.age).toBe(18);
85
- });
86
-
87
- it('should delete an object', async () => {
88
- const [charlie] = await driver.find('users', { where: { name: 'Charlie' } });
89
- await driver.delete('users', charlie.id);
90
-
91
- const deleted = await driver.findOne('users', charlie.id as any);
92
- expect(deleted).toBeNull();
93
- });
94
-
95
- it('should count objects', async () => {
96
- const count = await driver.count('users', { where: { age: 17 } } as any);
97
- expect(count).toBe(2);
98
- });
99
-
100
- it('should map _id to id if provided', async () => {
101
- const created = await driver.create('users', { _id: 'custom-id', name: 'Frank', age: 40 });
102
-
103
- expect(created.id).toBe('custom-id');
104
- const fetched = await driver.findOne('users', 'custom-id' as any);
105
- expect(fetched).toBeDefined();
106
- expect(fetched.name).toBe('Frank');
107
- });
108
- });