@objectstack/driver-sql 4.0.3 → 4.0.5
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/README.md +476 -0
- package/dist/index.d.mts +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +16 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +16 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +34 -8
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -44
- package/src/index.ts +0 -30
- package/src/sql-driver-advanced.test.ts +0 -499
- package/src/sql-driver-introspection.test.ts +0 -164
- package/src/sql-driver-queryast.test.ts +0 -243
- package/src/sql-driver-schema.test.ts +0 -313
- package/src/sql-driver.test.ts +0 -108
- package/src/sql-driver.ts +0 -1373
- package/tsconfig.json +0 -32
package/README.md
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
# @objectstack/driver-sql
|
|
2
|
+
|
|
3
|
+
SQL Driver for ObjectStack - Supports PostgreSQL, MySQL, SQLite via Knex.js.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-Database Support**: PostgreSQL, MySQL, SQLite, and other Knex-supported databases
|
|
8
|
+
- **Query Builder**: Powerful Knex.js query builder integration
|
|
9
|
+
- **Migrations**: Database schema migrations with version control
|
|
10
|
+
- **Connection Pooling**: Efficient connection management
|
|
11
|
+
- **Transactions**: Full ACID transaction support
|
|
12
|
+
- **Raw SQL**: Execute raw SQL when needed
|
|
13
|
+
- **Type-Safe**: Full TypeScript support with inferred types
|
|
14
|
+
- **Production-Ready**: Battle-tested Knex.js under the hood
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add @objectstack/driver-sql knex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Database-Specific Drivers
|
|
23
|
+
|
|
24
|
+
Install the driver for your database:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# PostgreSQL
|
|
28
|
+
pnpm add pg
|
|
29
|
+
|
|
30
|
+
# MySQL
|
|
31
|
+
pnpm add mysql2
|
|
32
|
+
|
|
33
|
+
# SQLite
|
|
34
|
+
pnpm add better-sqlite3
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Basic Usage
|
|
38
|
+
|
|
39
|
+
### PostgreSQL
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { defineStack } from '@objectstack/spec';
|
|
43
|
+
import { SqlDriver } from '@objectstack/driver-sql';
|
|
44
|
+
|
|
45
|
+
const stack = defineStack({
|
|
46
|
+
driver: new SqlDriver({
|
|
47
|
+
client: 'pg',
|
|
48
|
+
connection: {
|
|
49
|
+
host: 'localhost',
|
|
50
|
+
port: 5432,
|
|
51
|
+
user: 'postgres',
|
|
52
|
+
password: process.env.DB_PASSWORD,
|
|
53
|
+
database: 'myapp',
|
|
54
|
+
},
|
|
55
|
+
pool: {
|
|
56
|
+
min: 2,
|
|
57
|
+
max: 10,
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### MySQL
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
const stack = defineStack({
|
|
67
|
+
driver: new SqlDriver({
|
|
68
|
+
client: 'mysql2',
|
|
69
|
+
connection: {
|
|
70
|
+
host: 'localhost',
|
|
71
|
+
port: 3306,
|
|
72
|
+
user: 'root',
|
|
73
|
+
password: process.env.DB_PASSWORD,
|
|
74
|
+
database: 'myapp',
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### SQLite
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const stack = defineStack({
|
|
84
|
+
driver: new SqlDriver({
|
|
85
|
+
client: 'better-sqlite3',
|
|
86
|
+
connection: {
|
|
87
|
+
filename: './data/app.db',
|
|
88
|
+
},
|
|
89
|
+
useNullAsDefault: true,
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration Options
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
interface SQLDriverConfig {
|
|
98
|
+
/** Knex client (pg, mysql2, better-sqlite3, etc.) */
|
|
99
|
+
client: string;
|
|
100
|
+
|
|
101
|
+
/** Database connection config */
|
|
102
|
+
connection: {
|
|
103
|
+
host?: string;
|
|
104
|
+
port?: number;
|
|
105
|
+
user?: string;
|
|
106
|
+
password?: string;
|
|
107
|
+
database?: string;
|
|
108
|
+
filename?: string; // For SQLite
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Connection pool settings */
|
|
112
|
+
pool?: {
|
|
113
|
+
min?: number;
|
|
114
|
+
max?: number;
|
|
115
|
+
idleTimeoutMillis?: number;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/** Use NULL as default for unsupported features (SQLite) */
|
|
119
|
+
useNullAsDefault?: boolean;
|
|
120
|
+
|
|
121
|
+
/** Enable query debugging */
|
|
122
|
+
debug?: boolean;
|
|
123
|
+
|
|
124
|
+
/** Migrations configuration */
|
|
125
|
+
migrations?: {
|
|
126
|
+
directory?: string;
|
|
127
|
+
tableName?: string;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Database Operations
|
|
133
|
+
|
|
134
|
+
The SQL driver implements the standard ObjectStack driver interface:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
import type { IDriver } from '@objectstack/spec';
|
|
138
|
+
|
|
139
|
+
// All standard operations are supported:
|
|
140
|
+
// find, findOne, insert, update, delete, count
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Advanced Queries
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// The SQL driver supports all ObjectQL query features:
|
|
147
|
+
const results = await kernel.getDriver().find({
|
|
148
|
+
object: 'opportunity',
|
|
149
|
+
filters: [
|
|
150
|
+
{ field: 'amount', operator: 'gte', value: 10000 },
|
|
151
|
+
{ field: 'stage', operator: 'in', value: ['proposal', 'negotiation'] },
|
|
152
|
+
],
|
|
153
|
+
sort: [{ field: 'amount', direction: 'desc' }],
|
|
154
|
+
limit: 100,
|
|
155
|
+
offset: 0,
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Migrations
|
|
160
|
+
|
|
161
|
+
### Creating Migrations
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// migrations/001_create_users.ts
|
|
165
|
+
export async function up(knex) {
|
|
166
|
+
await knex.schema.createTable('objectstack_user', (table) => {
|
|
167
|
+
table.string('id').primary();
|
|
168
|
+
table.string('name').notNullable();
|
|
169
|
+
table.string('email').notNullable().unique();
|
|
170
|
+
table.timestamps(true, true);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function down(knex) {
|
|
175
|
+
await knex.schema.dropTable('objectstack_user');
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Running Migrations
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
# Run all pending migrations
|
|
183
|
+
npx knex migrate:latest
|
|
184
|
+
|
|
185
|
+
# Rollback last migration
|
|
186
|
+
npx knex migrate:rollback
|
|
187
|
+
|
|
188
|
+
# Check migration status
|
|
189
|
+
npx knex migrate:status
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Migration Configuration
|
|
193
|
+
|
|
194
|
+
Create `knexfile.js` in your project root:
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
module.exports = {
|
|
198
|
+
development: {
|
|
199
|
+
client: 'pg',
|
|
200
|
+
connection: {
|
|
201
|
+
host: 'localhost',
|
|
202
|
+
user: 'postgres',
|
|
203
|
+
password: process.env.DB_PASSWORD,
|
|
204
|
+
database: 'myapp_dev',
|
|
205
|
+
},
|
|
206
|
+
migrations: {
|
|
207
|
+
directory: './migrations',
|
|
208
|
+
tableName: 'objectstack_migrations',
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
production: {
|
|
212
|
+
client: 'pg',
|
|
213
|
+
connection: process.env.DATABASE_URL,
|
|
214
|
+
pool: {
|
|
215
|
+
min: 2,
|
|
216
|
+
max: 10,
|
|
217
|
+
},
|
|
218
|
+
migrations: {
|
|
219
|
+
directory: './migrations',
|
|
220
|
+
tableName: 'objectstack_migrations',
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Transactions
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
const driver = kernel.getDriver();
|
|
230
|
+
|
|
231
|
+
await driver.transaction(async (trx) => {
|
|
232
|
+
// All operations within this callback use the same transaction
|
|
233
|
+
const account = await trx.insert({
|
|
234
|
+
object: 'account',
|
|
235
|
+
data: { name: 'Acme Corp' },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
await trx.insert({
|
|
239
|
+
object: 'contact',
|
|
240
|
+
data: {
|
|
241
|
+
name: 'John Doe',
|
|
242
|
+
account_id: account.id,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// If an error is thrown, all changes are rolled back
|
|
247
|
+
// If successful, changes are committed
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Raw SQL Queries
|
|
252
|
+
|
|
253
|
+
When ObjectQL isn't sufficient, execute raw SQL:
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const driver = kernel.getDriver();
|
|
257
|
+
|
|
258
|
+
// Raw query
|
|
259
|
+
const results = await driver.raw(`
|
|
260
|
+
SELECT
|
|
261
|
+
c.name,
|
|
262
|
+
COUNT(o.id) as opportunity_count,
|
|
263
|
+
SUM(o.amount) as total_revenue
|
|
264
|
+
FROM objectstack_account c
|
|
265
|
+
LEFT JOIN objectstack_opportunity o ON o.account_id = c.id
|
|
266
|
+
WHERE o.stage = 'closed_won'
|
|
267
|
+
GROUP BY c.id, c.name
|
|
268
|
+
ORDER BY total_revenue DESC
|
|
269
|
+
LIMIT 10
|
|
270
|
+
`);
|
|
271
|
+
|
|
272
|
+
// Raw query with parameters (prevent SQL injection)
|
|
273
|
+
const results = await driver.raw(
|
|
274
|
+
'SELECT * FROM objectstack_user WHERE email = ?',
|
|
275
|
+
['user@example.com']
|
|
276
|
+
);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Database-Specific Features
|
|
280
|
+
|
|
281
|
+
### PostgreSQL Features
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
// Use PostgreSQL-specific features
|
|
285
|
+
const results = await driver.raw(`
|
|
286
|
+
SELECT * FROM objectstack_opportunity
|
|
287
|
+
WHERE data @> '{"industry": "Technology"}'::jsonb
|
|
288
|
+
`);
|
|
289
|
+
|
|
290
|
+
// Full-text search
|
|
291
|
+
const results = await driver.raw(`
|
|
292
|
+
SELECT * FROM objectstack_article
|
|
293
|
+
WHERE to_tsvector('english', title || ' ' || body) @@ to_tsquery('objectstack')
|
|
294
|
+
`);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### MySQL Features
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Use MySQL-specific features
|
|
301
|
+
const results = await driver.raw(`
|
|
302
|
+
SELECT * FROM objectstack_product
|
|
303
|
+
WHERE MATCH(name, description) AGAINST ('widget' IN NATURAL LANGUAGE MODE)
|
|
304
|
+
`);
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Connection Management
|
|
308
|
+
|
|
309
|
+
```typescript
|
|
310
|
+
// Get underlying Knex instance
|
|
311
|
+
const knex = driver.getKnex();
|
|
312
|
+
|
|
313
|
+
// Check connection
|
|
314
|
+
await driver.checkConnection();
|
|
315
|
+
|
|
316
|
+
// Close all connections
|
|
317
|
+
await driver.destroy();
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Performance Optimization
|
|
321
|
+
|
|
322
|
+
### Indexes
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Create index migration
|
|
326
|
+
export async function up(knex) {
|
|
327
|
+
await knex.schema.table('objectstack_opportunity', (table) => {
|
|
328
|
+
table.index('account_id');
|
|
329
|
+
table.index('stage');
|
|
330
|
+
table.index(['created_at', 'stage']); // Composite index
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Query Optimization
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
// Use explain to analyze queries
|
|
339
|
+
const plan = await driver.raw('EXPLAIN ANALYZE SELECT ...');
|
|
340
|
+
|
|
341
|
+
// Create covering indexes for frequently accessed columns
|
|
342
|
+
// Use partial indexes for filtered queries (PostgreSQL)
|
|
343
|
+
await knex.raw(`
|
|
344
|
+
CREATE INDEX idx_active_opportunities
|
|
345
|
+
ON objectstack_opportunity(account_id, amount)
|
|
346
|
+
WHERE stage NOT IN ('closed_won', 'closed_lost')
|
|
347
|
+
`);
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Best Practices
|
|
351
|
+
|
|
352
|
+
1. **Connection Pooling**: Configure appropriate pool size based on load
|
|
353
|
+
2. **Migrations**: Always use migrations for schema changes, never raw DDL
|
|
354
|
+
3. **Transactions**: Use transactions for multi-step operations
|
|
355
|
+
4. **Prepared Statements**: Use parameterized queries to prevent SQL injection
|
|
356
|
+
5. **Indexes**: Create indexes on frequently queried fields
|
|
357
|
+
6. **Monitoring**: Monitor slow query logs and connection pool metrics
|
|
358
|
+
7. **Backups**: Implement regular database backups
|
|
359
|
+
|
|
360
|
+
## Environment-Specific Configuration
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
// config/database.ts
|
|
364
|
+
export const getDatabaseConfig = () => {
|
|
365
|
+
const env = process.env.NODE_ENV || 'development';
|
|
366
|
+
|
|
367
|
+
const configs = {
|
|
368
|
+
development: {
|
|
369
|
+
client: 'better-sqlite3',
|
|
370
|
+
connection: { filename: './data/dev.db' },
|
|
371
|
+
useNullAsDefault: true,
|
|
372
|
+
debug: true,
|
|
373
|
+
},
|
|
374
|
+
test: {
|
|
375
|
+
client: 'better-sqlite3',
|
|
376
|
+
connection: { filename: ':memory:' },
|
|
377
|
+
useNullAsDefault: true,
|
|
378
|
+
},
|
|
379
|
+
production: {
|
|
380
|
+
client: 'pg',
|
|
381
|
+
connection: process.env.DATABASE_URL,
|
|
382
|
+
pool: { min: 2, max: 10 },
|
|
383
|
+
ssl: { rejectUnauthorized: false },
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
return configs[env] || configs.development;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const stack = defineStack({
|
|
391
|
+
driver: DriverSQL.configure(getDatabaseConfig()),
|
|
392
|
+
});
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
## Troubleshooting
|
|
396
|
+
|
|
397
|
+
### Connection Issues
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// Test database connection
|
|
401
|
+
try {
|
|
402
|
+
await driver.checkConnection();
|
|
403
|
+
console.log('Database connected successfully');
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error('Database connection failed:', error);
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Migration Errors
|
|
410
|
+
|
|
411
|
+
```bash
|
|
412
|
+
# Check migration status
|
|
413
|
+
npx knex migrate:status
|
|
414
|
+
|
|
415
|
+
# Rollback and re-run
|
|
416
|
+
npx knex migrate:rollback
|
|
417
|
+
npx knex migrate:latest
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### Query Debugging
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
// Enable query logging
|
|
424
|
+
const stack = defineStack({
|
|
425
|
+
driver: DriverSQL.configure({
|
|
426
|
+
client: 'pg',
|
|
427
|
+
connection: { /* ... */ },
|
|
428
|
+
debug: true, // Log all queries
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
## Deployment
|
|
434
|
+
|
|
435
|
+
### Heroku PostgreSQL
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
# Heroku automatically provides DATABASE_URL
|
|
439
|
+
heroku addons:create heroku-postgresql:hobby-dev
|
|
440
|
+
|
|
441
|
+
# Run migrations on deployment
|
|
442
|
+
echo "npx knex migrate:latest" > Procfile.release
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Railway PostgreSQL
|
|
446
|
+
|
|
447
|
+
```bash
|
|
448
|
+
# Use Railway's DATABASE_URL
|
|
449
|
+
railway up
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Vercel PostgreSQL
|
|
453
|
+
|
|
454
|
+
```typescript
|
|
455
|
+
// Vercel uses connection pooling
|
|
456
|
+
import { createClient } from '@vercel/postgres';
|
|
457
|
+
|
|
458
|
+
const stack = defineStack({
|
|
459
|
+
driver: DriverSQL.configure({
|
|
460
|
+
client: 'pg',
|
|
461
|
+
connection: process.env.POSTGRES_URL,
|
|
462
|
+
}),
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## License
|
|
467
|
+
|
|
468
|
+
Apache-2.0
|
|
469
|
+
|
|
470
|
+
## See Also
|
|
471
|
+
|
|
472
|
+
- [Knex.js Documentation](https://knexjs.org/)
|
|
473
|
+
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
|
|
474
|
+
- [MySQL Documentation](https://dev.mysql.com/doc/)
|
|
475
|
+
- [@objectstack/driver-turso](../driver-turso/) - Edge-first SQLite alternative
|
|
476
|
+
- [@objectstack/driver-memory](../driver-memory/) - In-memory driver for testing
|
package/dist/index.d.mts
CHANGED
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ __export(index_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
37
|
|
|
38
38
|
// src/sql-driver.ts
|
|
39
|
+
var import_system = require("@objectstack/spec/system");
|
|
39
40
|
var import_knex = __toESM(require("knex"));
|
|
40
41
|
var import_nanoid = require("nanoid");
|
|
41
42
|
var DEFAULT_ID_LENGTH = 16;
|
|
@@ -109,7 +110,7 @@ var SqlDriver = class {
|
|
|
109
110
|
// Lifecycle
|
|
110
111
|
// ===================================
|
|
111
112
|
async connect() {
|
|
112
|
-
|
|
113
|
+
await this.ensureDatabaseExists();
|
|
113
114
|
}
|
|
114
115
|
async checkHealth() {
|
|
115
116
|
try {
|
|
@@ -446,7 +447,7 @@ var SqlDriver = class {
|
|
|
446
447
|
async initObjects(objects) {
|
|
447
448
|
await this.ensureDatabaseExists();
|
|
448
449
|
for (const obj of objects) {
|
|
449
|
-
const tableName =
|
|
450
|
+
const tableName = import_system.StorageNameMapping.resolveTableName(obj);
|
|
450
451
|
const jsonCols = [];
|
|
451
452
|
const booleanCols = [];
|
|
452
453
|
if (obj.fields) {
|
|
@@ -814,7 +815,19 @@ var SqlDriver = class {
|
|
|
814
815
|
}
|
|
815
816
|
// ── Database helpers ────────────────────────────────────────────────────────
|
|
816
817
|
async ensureDatabaseExists() {
|
|
817
|
-
if (this.isSqlite)
|
|
818
|
+
if (this.isSqlite) {
|
|
819
|
+
const conn = this.config.connection;
|
|
820
|
+
const filename = typeof conn === "string" ? conn : conn?.filename;
|
|
821
|
+
if (filename && filename !== ":memory:" && !filename.startsWith(":")) {
|
|
822
|
+
const { dirname } = await import("path");
|
|
823
|
+
const { mkdir } = await import("fs/promises");
|
|
824
|
+
const dir = dirname(filename);
|
|
825
|
+
if (dir && dir !== ".") {
|
|
826
|
+
await mkdir(dir, { recursive: true });
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
818
831
|
if (!this.isPostgres && !this.isMysql) return;
|
|
819
832
|
try {
|
|
820
833
|
await this.knex.raw("SELECT 1");
|