@entergreat/pg-light 1.0.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.
- package/README.md +928 -0
- package/example-knex.js +246 -0
- package/example-model.js +103 -0
- package/example.js +38 -0
- package/package.json +48 -0
- package/src/index.js +117 -0
- package/src/model.js +137 -0
package/README.md
ADDED
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
# @entergreat/pg-light ๐
|
|
2
|
+
|
|
3
|
+
A lightweight PostgreSQL connection utility with one-line setup. Simple, clean, and production-ready.
|
|
4
|
+
|
|
5
|
+
## Features โจ
|
|
6
|
+
|
|
7
|
+
- โก **One-line Connection** - Connect with a single function call
|
|
8
|
+
- ๐ **Connection String or Config** - Flexible connection options
|
|
9
|
+
- ๐ **Connection Pooling** - Built-in connection pool management
|
|
10
|
+
- ๐ **Transaction Support** - Easy transaction handling with auto-rollback
|
|
11
|
+
- ๐ **Query Interface** - Simple query execution
|
|
12
|
+
- ๐จ **Mongoose-like Models** - Friendly API for common CRUD operations
|
|
13
|
+
- ๐จ **Knex Query Builder** - Powerful chainable API for complex queries (NEW!)
|
|
14
|
+
- ๐ **Integrated Logging** - Uses `logger-standard` for consistent logging
|
|
15
|
+
- ๐งน **Clean Code** - All functions โค7 lines
|
|
16
|
+
- ๐ฏ **TypeScript Ready** - JSDoc types for better IDE support
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @entergreat/pg-light
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
### Raw SQL Queries
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
30
|
+
|
|
31
|
+
// One-liner connection with connection string
|
|
32
|
+
const db = await connectDB('postgresql://user:pass@localhost:5432/mydb');
|
|
33
|
+
|
|
34
|
+
// Query data
|
|
35
|
+
const result = await db.query('SELECT * FROM users WHERE id = $1', [123]);
|
|
36
|
+
console.log(result.rows);
|
|
37
|
+
|
|
38
|
+
// Close connection
|
|
39
|
+
await db.disconnect();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Mongoose-like Models (For Simple CRUD)
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
46
|
+
|
|
47
|
+
const db = await connectDB('postgresql://user:pass@localhost:5432/mydb');
|
|
48
|
+
|
|
49
|
+
// Create a model
|
|
50
|
+
const User = db.model('users');
|
|
51
|
+
|
|
52
|
+
// CRUD operations - No SQL needed!
|
|
53
|
+
const newUser = await User.create({ name: 'John', email: 'john@example.com' });
|
|
54
|
+
const users = await User.find({ active: true });
|
|
55
|
+
const user = await User.findById(1);
|
|
56
|
+
await User.updateById(1, { name: 'Jane' });
|
|
57
|
+
await User.deleteById(1);
|
|
58
|
+
|
|
59
|
+
await db.disconnect();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Knex Query Builder (For Complex Queries)
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
66
|
+
|
|
67
|
+
const db = await connectDB('postgresql://user:pass@localhost:5432/mydb');
|
|
68
|
+
|
|
69
|
+
// Complex queries with Knex chainable API
|
|
70
|
+
const popularPosts = await db.builder('posts')
|
|
71
|
+
.select('posts.*', 'users.name as author')
|
|
72
|
+
.join('users', 'posts.user_id', 'users.id')
|
|
73
|
+
.where('posts.views', '>', 1000)
|
|
74
|
+
.whereIn('posts.category', ['tech', 'science'])
|
|
75
|
+
.orderBy('posts.views', 'desc')
|
|
76
|
+
.limit(10);
|
|
77
|
+
|
|
78
|
+
// Aggregations
|
|
79
|
+
const stats = await db.builder('orders')
|
|
80
|
+
.count('* as total_orders')
|
|
81
|
+
.sum('amount as revenue')
|
|
82
|
+
.where('status', 'completed');
|
|
83
|
+
|
|
84
|
+
// Transactions
|
|
85
|
+
await db.knex.transaction(async (trx) => {
|
|
86
|
+
const [user] = await trx('users').insert({ name: 'Alice' }).returning('*');
|
|
87
|
+
await trx('posts').insert({ user_id: user.id, title: 'First Post' });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await db.disconnect();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## API
|
|
94
|
+
|
|
95
|
+
### `connectDB(config, options?)`
|
|
96
|
+
|
|
97
|
+
Connects to PostgreSQL database and returns a `PgLight` instance.
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
- `config` - Connection string or config object
|
|
101
|
+
- `options.service` - Logger service name (default: `'pg-light'`)
|
|
102
|
+
- `options.logLevel` - Log level: `'debug'`, `'info'`, `'warn'`, `'error'` (default: `'info'`)
|
|
103
|
+
|
|
104
|
+
**Returns:** `Promise<PgLight>`
|
|
105
|
+
|
|
106
|
+
### Connection Methods
|
|
107
|
+
|
|
108
|
+
#### With Connection String
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
const db = await connectDB('postgresql://user:pass@localhost:5432/mydb');
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### With Config Object
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
const db = await connectDB({
|
|
118
|
+
host: 'localhost',
|
|
119
|
+
port: 5432,
|
|
120
|
+
database: 'mydb',
|
|
121
|
+
user: 'myuser',
|
|
122
|
+
password: 'mypassword',
|
|
123
|
+
max: 20, // Max connections in pool
|
|
124
|
+
idleTimeoutMillis: 30000,
|
|
125
|
+
connectionTimeoutMillis: 2000
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
#### With Custom Logger Options
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const db = await connectDB(config, {
|
|
133
|
+
service: 'my-api',
|
|
134
|
+
logLevel: 'debug'
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### PgLight Methods
|
|
139
|
+
|
|
140
|
+
#### `query(text, params?)`
|
|
141
|
+
|
|
142
|
+
Execute a SQL query.
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
// Simple query
|
|
146
|
+
const result = await db.query('SELECT * FROM users');
|
|
147
|
+
|
|
148
|
+
// Parameterized query (prevents SQL injection)
|
|
149
|
+
const result = await db.query(
|
|
150
|
+
'SELECT * FROM users WHERE email = $1',
|
|
151
|
+
['john@example.com']
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Access results
|
|
155
|
+
console.log(result.rows); // Array of row objects
|
|
156
|
+
console.log(result.rowCount); // Number of rows
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### `transaction(callback)`
|
|
160
|
+
|
|
161
|
+
Execute queries within a transaction. Automatically commits on success or rolls back on error.
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
await db.transaction(async (client) => {
|
|
165
|
+
await client.query('INSERT INTO users (name, email) VALUES ($1, $2)',
|
|
166
|
+
['John Doe', 'john@example.com']);
|
|
167
|
+
|
|
168
|
+
await client.query('INSERT INTO logs (action, user_id) VALUES ($1, $2)',
|
|
169
|
+
['user_created', 1]);
|
|
170
|
+
|
|
171
|
+
// If any query fails, all changes are rolled back
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Error Handling:**
|
|
176
|
+
|
|
177
|
+
```js
|
|
178
|
+
try {
|
|
179
|
+
await db.transaction(async (client) => {
|
|
180
|
+
await client.query('INSERT INTO users (name) VALUES ($1)', ['John']);
|
|
181
|
+
throw new Error('Something went wrong');
|
|
182
|
+
// Transaction automatically rolls back
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('Transaction failed:', error.message);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `disconnect()`
|
|
190
|
+
|
|
191
|
+
Close all connections in the pool.
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
await db.disconnect();
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### `getPool()`
|
|
198
|
+
|
|
199
|
+
Get the underlying `pg.Pool` instance for advanced usage.
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
const pool = db.getPool();
|
|
203
|
+
|
|
204
|
+
// Use native pg.Pool methods
|
|
205
|
+
pool.on('error', (err) => {
|
|
206
|
+
console.error('Unexpected pool error', err);
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### `model(tableName)`
|
|
211
|
+
|
|
212
|
+
Create a Mongoose-like model for a database table. Returns a `Model` instance with CRUD methods.
|
|
213
|
+
|
|
214
|
+
```js
|
|
215
|
+
const User = db.model('users');
|
|
216
|
+
const Post = db.model('posts');
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
#### `builder(tableName)`
|
|
220
|
+
|
|
221
|
+
Create a Knex query builder for complex queries. Returns a Knex query builder instance.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
// SELECT with WHERE and JOIN
|
|
225
|
+
const results = await db.builder('posts')
|
|
226
|
+
.select('posts.*', 'users.name as author')
|
|
227
|
+
.join('users', 'posts.user_id', 'users.id')
|
|
228
|
+
.where('posts.published', true)
|
|
229
|
+
.orderBy('posts.created_at', 'desc')
|
|
230
|
+
.limit(10);
|
|
231
|
+
|
|
232
|
+
// Aggregations
|
|
233
|
+
const stats = await db.builder('orders')
|
|
234
|
+
.select('user_id')
|
|
235
|
+
.count('* as order_count')
|
|
236
|
+
.sum('total as revenue')
|
|
237
|
+
.groupBy('user_id')
|
|
238
|
+
.having('order_count', '>', 5);
|
|
239
|
+
|
|
240
|
+
// INSERT
|
|
241
|
+
const [user] = await db.builder('users')
|
|
242
|
+
.insert({ name: 'Alice', email: 'alice@example.com' })
|
|
243
|
+
.returning('*');
|
|
244
|
+
|
|
245
|
+
// UPDATE
|
|
246
|
+
await db.builder('users')
|
|
247
|
+
.where({ id: 1 })
|
|
248
|
+
.update({ last_login: db.knex.fn.now() });
|
|
249
|
+
|
|
250
|
+
// DELETE
|
|
251
|
+
await db.builder('users')
|
|
252
|
+
.where('active', false)
|
|
253
|
+
.delete();
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
See [Knex Query Builder Documentation](https://knexjs.org/guide/query-builder.html) for full API.
|
|
257
|
+
|
|
258
|
+
#### `knex`
|
|
259
|
+
|
|
260
|
+
Get the Knex instance for advanced usage, raw queries, and transactions.
|
|
261
|
+
|
|
262
|
+
```js
|
|
263
|
+
// Raw queries
|
|
264
|
+
const result = await db.knex.raw('SELECT NOW()');
|
|
265
|
+
|
|
266
|
+
// Transactions
|
|
267
|
+
await db.knex.transaction(async (trx) => {
|
|
268
|
+
const [user] = await trx('users').insert({ name: 'Bob' }).returning('*');
|
|
269
|
+
await trx('logs').insert({ user_id: user.id, action: 'created' });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Knex utility functions
|
|
273
|
+
const now = db.knex.fn.now();
|
|
274
|
+
const count = db.knex.count('* as total');
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Model Methods
|
|
278
|
+
|
|
279
|
+
The Model class provides a friendly API for common database operations without writing SQL.
|
|
280
|
+
|
|
281
|
+
#### `create(data)`
|
|
282
|
+
|
|
283
|
+
Insert a new record and return it.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
const user = await User.create({
|
|
287
|
+
name: 'Alice',
|
|
288
|
+
email: 'alice@example.com',
|
|
289
|
+
age: 25
|
|
290
|
+
});
|
|
291
|
+
// Returns: { id: 1, name: 'Alice', email: 'alice@example.com', age: 25 }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### `find(conditions?, options?)`
|
|
295
|
+
|
|
296
|
+
Find multiple records matching conditions.
|
|
297
|
+
|
|
298
|
+
```js
|
|
299
|
+
// Find all users
|
|
300
|
+
const allUsers = await User.find();
|
|
301
|
+
|
|
302
|
+
// Find with conditions
|
|
303
|
+
const activeUsers = await User.find({ active: true });
|
|
304
|
+
|
|
305
|
+
// Find with pagination
|
|
306
|
+
const users = await User.find({ role: 'admin' }, { limit: 10, offset: 20 });
|
|
307
|
+
|
|
308
|
+
// Find with limit only
|
|
309
|
+
const recentUsers = await User.find({}, { limit: 5 });
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
#### `findOne(conditions)`
|
|
313
|
+
|
|
314
|
+
Find a single record matching conditions.
|
|
315
|
+
|
|
316
|
+
```js
|
|
317
|
+
const user = await User.findOne({ email: 'alice@example.com' });
|
|
318
|
+
// Returns: { id: 1, name: 'Alice', ... } or null
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
#### `findById(id)`
|
|
322
|
+
|
|
323
|
+
Find a record by its primary key (id).
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
const user = await User.findById(1);
|
|
327
|
+
// Returns: { id: 1, name: 'Alice', ... } or null
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
#### `update(conditions, data)`
|
|
331
|
+
|
|
332
|
+
Update records matching conditions and return updated records.
|
|
333
|
+
|
|
334
|
+
```js
|
|
335
|
+
const updatedUsers = await User.update(
|
|
336
|
+
{ active: false },
|
|
337
|
+
{ status: 'archived' }
|
|
338
|
+
);
|
|
339
|
+
// Returns: [{ id: 5, status: 'archived', ... }, ...]
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### `updateById(id, data)`
|
|
343
|
+
|
|
344
|
+
Update a single record by id and return it.
|
|
345
|
+
|
|
346
|
+
```js
|
|
347
|
+
const user = await User.updateById(1, { last_login: new Date() });
|
|
348
|
+
// Returns: { id: 1, last_login: '2026-04-12T...', ... } or null
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### `delete(conditions)`
|
|
352
|
+
|
|
353
|
+
Delete records matching conditions and return deleted records.
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
const deletedUsers = await User.delete({ active: false });
|
|
357
|
+
// Returns: [{ id: 10, active: false, ... }, ...]
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
#### `deleteById(id)`
|
|
361
|
+
|
|
362
|
+
Delete a single record by id and return it.
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
const user = await User.deleteById(1);
|
|
366
|
+
// Returns: { id: 1, ... } or null
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### `count(conditions?)`
|
|
370
|
+
|
|
371
|
+
Count records matching conditions.
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
// Count all records
|
|
375
|
+
const totalUsers = await User.count();
|
|
376
|
+
// Returns: 42
|
|
377
|
+
|
|
378
|
+
// Count with conditions
|
|
379
|
+
const activeCount = await User.count({ active: true });
|
|
380
|
+
// Returns: 35
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Usage Examples
|
|
384
|
+
|
|
385
|
+
### Model API (Mongoose-like) - Recommended
|
|
386
|
+
|
|
387
|
+
```js
|
|
388
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
389
|
+
|
|
390
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
391
|
+
|
|
392
|
+
// Create a model
|
|
393
|
+
const User = db.model('users');
|
|
394
|
+
|
|
395
|
+
// CREATE - Insert records
|
|
396
|
+
const alice = await User.create({
|
|
397
|
+
name: 'Alice Smith',
|
|
398
|
+
email: 'alice@example.com',
|
|
399
|
+
active: true
|
|
400
|
+
});
|
|
401
|
+
console.log('Created:', alice);
|
|
402
|
+
|
|
403
|
+
// READ - Find records
|
|
404
|
+
const activeUsers = await User.find({ active: true });
|
|
405
|
+
console.log('Active users:', activeUsers);
|
|
406
|
+
|
|
407
|
+
const user = await User.findOne({ email: 'alice@example.com' });
|
|
408
|
+
console.log('Found user:', user);
|
|
409
|
+
|
|
410
|
+
const userById = await User.findById(1);
|
|
411
|
+
console.log('User #1:', userById);
|
|
412
|
+
|
|
413
|
+
// Pagination
|
|
414
|
+
const page1 = await User.find({}, { limit: 10, offset: 0 });
|
|
415
|
+
const page2 = await User.find({}, { limit: 10, offset: 10 });
|
|
416
|
+
|
|
417
|
+
// UPDATE - Modify records
|
|
418
|
+
await User.updateById(1, { last_login: new Date() });
|
|
419
|
+
|
|
420
|
+
const updated = await User.update(
|
|
421
|
+
{ active: false },
|
|
422
|
+
{ status: 'archived' }
|
|
423
|
+
);
|
|
424
|
+
console.log('Updated:', updated);
|
|
425
|
+
|
|
426
|
+
// DELETE - Remove records
|
|
427
|
+
await User.deleteById(999);
|
|
428
|
+
|
|
429
|
+
const deleted = await User.delete({ active: false });
|
|
430
|
+
console.log('Deleted:', deleted);
|
|
431
|
+
|
|
432
|
+
// COUNT - Count records
|
|
433
|
+
const total = await User.count();
|
|
434
|
+
const activeCount = await User.count({ active: true });
|
|
435
|
+
console.log(`${activeCount} active out of ${total} total users`);
|
|
436
|
+
|
|
437
|
+
await db.disconnect();
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Model with Relationships
|
|
441
|
+
|
|
442
|
+
```js
|
|
443
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
444
|
+
|
|
445
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
446
|
+
|
|
447
|
+
const User = db.model('users');
|
|
448
|
+
const Post = db.model('posts');
|
|
449
|
+
const Comment = db.model('comments');
|
|
450
|
+
|
|
451
|
+
// Create a user
|
|
452
|
+
const john = await User.create({
|
|
453
|
+
name: 'John Doe',
|
|
454
|
+
email: 'john@example.com'
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Create posts for the user
|
|
458
|
+
const post1 = await Post.create({
|
|
459
|
+
user_id: john.id,
|
|
460
|
+
title: 'My First Post',
|
|
461
|
+
content: 'Hello World!',
|
|
462
|
+
published: true
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
const post2 = await Post.create({
|
|
466
|
+
user_id: john.id,
|
|
467
|
+
title: 'Another Post',
|
|
468
|
+
content: 'More content...',
|
|
469
|
+
published: false
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// Create comments
|
|
473
|
+
await Comment.create({
|
|
474
|
+
post_id: post1.id,
|
|
475
|
+
user_id: john.id,
|
|
476
|
+
content: 'Great post!'
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Find all posts by user
|
|
480
|
+
const userPosts = await Post.find({ user_id: john.id });
|
|
481
|
+
console.log(`John has ${userPosts.length} posts`);
|
|
482
|
+
|
|
483
|
+
// Find published posts
|
|
484
|
+
const publishedPosts = await Post.find({ published: true });
|
|
485
|
+
|
|
486
|
+
// Complex queries still use raw SQL
|
|
487
|
+
const postsWithComments = await db.query(`
|
|
488
|
+
SELECT p.*, COUNT(c.id) as comment_count
|
|
489
|
+
FROM posts p
|
|
490
|
+
LEFT JOIN comments c ON p.id = c.post_id
|
|
491
|
+
WHERE p.user_id = $1
|
|
492
|
+
GROUP BY p.id
|
|
493
|
+
`, [john.id]);
|
|
494
|
+
|
|
495
|
+
console.log('Posts with comment counts:', postsWithComments.rows);
|
|
496
|
+
|
|
497
|
+
await db.disconnect();
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Knex Query Builder Examples
|
|
501
|
+
|
|
502
|
+
```js
|
|
503
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
504
|
+
|
|
505
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
506
|
+
|
|
507
|
+
// SIMPLE QUERIES
|
|
508
|
+
|
|
509
|
+
// Select with WHERE
|
|
510
|
+
const activeUsers = await db.builder('users')
|
|
511
|
+
.select('*')
|
|
512
|
+
.where({ active: true })
|
|
513
|
+
.orderBy('created_at', 'desc');
|
|
514
|
+
|
|
515
|
+
// Select specific columns
|
|
516
|
+
const userEmails = await db.builder('users')
|
|
517
|
+
.select('id', 'name', 'email')
|
|
518
|
+
.where('age', '>', 25)
|
|
519
|
+
.limit(10);
|
|
520
|
+
|
|
521
|
+
// JOINS
|
|
522
|
+
|
|
523
|
+
const postsWithAuthors = await db.builder('posts')
|
|
524
|
+
.select('posts.*', 'users.name as author_name')
|
|
525
|
+
.join('users', 'posts.user_id', 'users.id')
|
|
526
|
+
.where('posts.published', true)
|
|
527
|
+
.orderBy('posts.views', 'desc')
|
|
528
|
+
.limit(20);
|
|
529
|
+
|
|
530
|
+
// Multiple JOINs with aggregation
|
|
531
|
+
const postsWithStats = await db.builder('posts')
|
|
532
|
+
.select('posts.*', 'users.name as author')
|
|
533
|
+
.count('comments.id as comment_count')
|
|
534
|
+
.leftJoin('users', 'posts.user_id', 'users.id')
|
|
535
|
+
.leftJoin('comments', 'posts.id', 'comments.post_id')
|
|
536
|
+
.groupBy('posts.id', 'users.name')
|
|
537
|
+
.having(db.knex.raw('COUNT(comments.id) > ?', [5]));
|
|
538
|
+
|
|
539
|
+
// COMPLEX WHERE CLAUSES
|
|
540
|
+
|
|
541
|
+
const filtered = await db.builder('users')
|
|
542
|
+
.select('*')
|
|
543
|
+
.where('active', true)
|
|
544
|
+
.andWhere('age', '>=', 18)
|
|
545
|
+
.whereIn('role', ['admin', 'moderator'])
|
|
546
|
+
.whereNotNull('email')
|
|
547
|
+
.where(function() {
|
|
548
|
+
this.where('name', 'like', '%John%')
|
|
549
|
+
.orWhere('email', 'like', '%john%');
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// SUBQUERIES
|
|
553
|
+
|
|
554
|
+
const prolificAuthors = await db.builder('users')
|
|
555
|
+
.select('users.*')
|
|
556
|
+
.whereIn('users.id', function() {
|
|
557
|
+
this.select('user_id')
|
|
558
|
+
.from('posts')
|
|
559
|
+
.groupBy('user_id')
|
|
560
|
+
.havingRaw('COUNT(*) > ?', [10]);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
// AGGREGATIONS
|
|
564
|
+
|
|
565
|
+
const orderStats = await db.builder('orders')
|
|
566
|
+
.select(
|
|
567
|
+
db.knex.raw('AVG(total) as avg_order'),
|
|
568
|
+
db.knex.raw('SUM(total) as revenue'),
|
|
569
|
+
db.knex.raw('MIN(total) as min_order'),
|
|
570
|
+
db.knex.raw('MAX(total) as max_order')
|
|
571
|
+
)
|
|
572
|
+
.where('status', 'completed')
|
|
573
|
+
.first();
|
|
574
|
+
|
|
575
|
+
// GROUP BY
|
|
576
|
+
|
|
577
|
+
const topSpenders = await db.builder('orders')
|
|
578
|
+
.select('user_id')
|
|
579
|
+
.count('* as order_count')
|
|
580
|
+
.sum('total as total_spent')
|
|
581
|
+
.groupBy('user_id')
|
|
582
|
+
.orderBy('total_spent', 'desc')
|
|
583
|
+
.limit(10);
|
|
584
|
+
|
|
585
|
+
// INSERT
|
|
586
|
+
|
|
587
|
+
const [newUser] = await db.builder('users')
|
|
588
|
+
.insert({
|
|
589
|
+
name: 'Alice',
|
|
590
|
+
email: 'alice@example.com',
|
|
591
|
+
created_at: db.knex.fn.now()
|
|
592
|
+
})
|
|
593
|
+
.returning('*');
|
|
594
|
+
|
|
595
|
+
// Bulk insert
|
|
596
|
+
const newUsers = await db.builder('users')
|
|
597
|
+
.insert([
|
|
598
|
+
{ name: 'Bob', email: 'bob@example.com' },
|
|
599
|
+
{ name: 'Charlie', email: 'charlie@example.com' }
|
|
600
|
+
])
|
|
601
|
+
.returning('*');
|
|
602
|
+
|
|
603
|
+
// UPDATE
|
|
604
|
+
|
|
605
|
+
await db.builder('users')
|
|
606
|
+
.where({ email: 'alice@example.com' })
|
|
607
|
+
.update({
|
|
608
|
+
last_login: db.knex.fn.now(),
|
|
609
|
+
login_count: db.knex.raw('login_count + 1')
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// DELETE
|
|
613
|
+
|
|
614
|
+
const deletedCount = await db.builder('users')
|
|
615
|
+
.where('active', false)
|
|
616
|
+
.andWhere('last_login', '<', db.knex.raw("NOW() - INTERVAL '1 year'"))
|
|
617
|
+
.delete();
|
|
618
|
+
|
|
619
|
+
// TRANSACTIONS
|
|
620
|
+
|
|
621
|
+
await db.knex.transaction(async (trx) => {
|
|
622
|
+
const [user] = await trx('users')
|
|
623
|
+
.insert({ name: 'David', email: 'david@example.com' })
|
|
624
|
+
.returning('*');
|
|
625
|
+
|
|
626
|
+
await trx('posts').insert([
|
|
627
|
+
{ user_id: user.id, title: 'Post 1' },
|
|
628
|
+
{ user_id: user.id, title: 'Post 2' }
|
|
629
|
+
]);
|
|
630
|
+
|
|
631
|
+
await trx('users')
|
|
632
|
+
.where({ id: user.id })
|
|
633
|
+
.update({ post_count: 2 });
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// PAGINATION
|
|
637
|
+
|
|
638
|
+
const page = 2;
|
|
639
|
+
const perPage = 10;
|
|
640
|
+
|
|
641
|
+
const results = await db.builder('users')
|
|
642
|
+
.select('*')
|
|
643
|
+
.where({ active: true })
|
|
644
|
+
.orderBy('created_at', 'desc')
|
|
645
|
+
.limit(perPage)
|
|
646
|
+
.offset((page - 1) * perPage);
|
|
647
|
+
|
|
648
|
+
const [{ total }] = await db.builder('users')
|
|
649
|
+
.count('* as total')
|
|
650
|
+
.where({ active: true });
|
|
651
|
+
|
|
652
|
+
console.log({
|
|
653
|
+
data: results,
|
|
654
|
+
page,
|
|
655
|
+
perPage,
|
|
656
|
+
total,
|
|
657
|
+
totalPages: Math.ceil(total / perPage)
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
await db.disconnect();
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### Which API to Use?
|
|
664
|
+
|
|
665
|
+
**Use Model API when:**
|
|
666
|
+
- โ
Simple CRUD operations (create, read, update, delete by ID)
|
|
667
|
+
- โ
Single table queries with basic conditions
|
|
668
|
+
- โ
You want the simplest, most readable code
|
|
669
|
+
|
|
670
|
+
**Use Knex Builder when:**
|
|
671
|
+
- โ
Complex queries with JOINs
|
|
672
|
+
- โ
Aggregations (COUNT, SUM, AVG, etc.)
|
|
673
|
+
- โ
Subqueries
|
|
674
|
+
- โ
GROUP BY / HAVING clauses
|
|
675
|
+
- โ
Complex WHERE conditions
|
|
676
|
+
- โ
You need chainable query building
|
|
677
|
+
|
|
678
|
+
**Use Raw SQL when:**
|
|
679
|
+
- โ
PostgreSQL-specific features (window functions, CTEs, etc.)
|
|
680
|
+
- โ
Highly optimized custom queries
|
|
681
|
+
- โ
Database migrations
|
|
682
|
+
- โ
You prefer writing SQL directly
|
|
683
|
+
|
|
684
|
+
**Mix and match as needed!** All three approaches work together seamlessly.
|
|
685
|
+
|
|
686
|
+
### Basic CRUD Operations (Raw SQL)
|
|
687
|
+
|
|
688
|
+
```js
|
|
689
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
690
|
+
|
|
691
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
692
|
+
|
|
693
|
+
// CREATE
|
|
694
|
+
const insertResult = await db.query(
|
|
695
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
|
|
696
|
+
['Alice Smith', 'alice@example.com']
|
|
697
|
+
);
|
|
698
|
+
console.log('Created user:', insertResult.rows[0]);
|
|
699
|
+
|
|
700
|
+
// READ
|
|
701
|
+
const users = await db.query('SELECT * FROM users WHERE active = $1', [true]);
|
|
702
|
+
console.log('Active users:', users.rows);
|
|
703
|
+
|
|
704
|
+
// UPDATE
|
|
705
|
+
const updateResult = await db.query(
|
|
706
|
+
'UPDATE users SET last_login = NOW() WHERE id = $1 RETURNING *',
|
|
707
|
+
[123]
|
|
708
|
+
);
|
|
709
|
+
console.log('Updated user:', updateResult.rows[0]);
|
|
710
|
+
|
|
711
|
+
// DELETE
|
|
712
|
+
await db.query('DELETE FROM users WHERE id = $1', [456]);
|
|
713
|
+
|
|
714
|
+
await db.disconnect();
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
### Transaction Example
|
|
718
|
+
|
|
719
|
+
```js
|
|
720
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
721
|
+
|
|
722
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
723
|
+
|
|
724
|
+
// Transfer money between accounts (atomic operation)
|
|
725
|
+
async function transferMoney(fromId, toId, amount) {
|
|
726
|
+
await db.transaction(async (client) => {
|
|
727
|
+
// Debit from account
|
|
728
|
+
await client.query(
|
|
729
|
+
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
|
|
730
|
+
[amount, fromId]
|
|
731
|
+
);
|
|
732
|
+
|
|
733
|
+
// Credit to account
|
|
734
|
+
await client.query(
|
|
735
|
+
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
|
|
736
|
+
[amount, toId]
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
// Log transaction
|
|
740
|
+
await client.query(
|
|
741
|
+
'INSERT INTO transactions (from_id, to_id, amount) VALUES ($1, $2, $3)',
|
|
742
|
+
[fromId, toId, amount]
|
|
743
|
+
);
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
await transferMoney(1, 2, 100.00);
|
|
749
|
+
console.log('Transfer successful');
|
|
750
|
+
} catch (error) {
|
|
751
|
+
console.error('Transfer failed:', error.message);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
await db.disconnect();
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Connection Pooling
|
|
758
|
+
|
|
759
|
+
```js
|
|
760
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
761
|
+
|
|
762
|
+
// Configure connection pool
|
|
763
|
+
const db = await connectDB({
|
|
764
|
+
host: 'localhost',
|
|
765
|
+
database: 'mydb',
|
|
766
|
+
user: 'myuser',
|
|
767
|
+
password: 'mypass',
|
|
768
|
+
max: 20, // Max 20 connections
|
|
769
|
+
idleTimeoutMillis: 30000, // Close idle connections after 30s
|
|
770
|
+
connectionTimeoutMillis: 2000 // Fail if connection takes > 2s
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// Pool automatically manages connections
|
|
774
|
+
// Multiple queries share the pool
|
|
775
|
+
const [users, orders, products] = await Promise.all([
|
|
776
|
+
db.query('SELECT * FROM users'),
|
|
777
|
+
db.query('SELECT * FROM orders'),
|
|
778
|
+
db.query('SELECT * FROM products')
|
|
779
|
+
]);
|
|
780
|
+
|
|
781
|
+
await db.disconnect();
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
### Custom Logging
|
|
785
|
+
|
|
786
|
+
```js
|
|
787
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
788
|
+
|
|
789
|
+
// Enable debug logging
|
|
790
|
+
const db = await connectDB(process.env.DATABASE_URL, {
|
|
791
|
+
service: 'my-api-db',
|
|
792
|
+
logLevel: 'debug'
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Logs will show:
|
|
796
|
+
// [my-api-db] Database connected successfully
|
|
797
|
+
// [my-api-db] Executing query: SELECT * FROM users
|
|
798
|
+
// [my-api-db] Query returned 42 rows
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
### Environment Variables
|
|
802
|
+
|
|
803
|
+
```js
|
|
804
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
805
|
+
|
|
806
|
+
// .env file:
|
|
807
|
+
// DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
|
|
808
|
+
|
|
809
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
810
|
+
|
|
811
|
+
// Or with separate vars:
|
|
812
|
+
const db = await connectDB({
|
|
813
|
+
host: process.env.DB_HOST,
|
|
814
|
+
port: process.env.DB_PORT,
|
|
815
|
+
database: process.env.DB_NAME,
|
|
816
|
+
user: process.env.DB_USER,
|
|
817
|
+
password: process.env.DB_PASSWORD
|
|
818
|
+
});
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
### Express.js Integration
|
|
822
|
+
|
|
823
|
+
```js
|
|
824
|
+
import express from 'express';
|
|
825
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
826
|
+
|
|
827
|
+
const app = express();
|
|
828
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
829
|
+
|
|
830
|
+
app.get('/users', async (req, res) => {
|
|
831
|
+
try {
|
|
832
|
+
const result = await db.query('SELECT * FROM users');
|
|
833
|
+
res.json(result.rows);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
res.status(500).json({ error: error.message });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
app.post('/users', async (req, res) => {
|
|
840
|
+
const { name, email } = req.body;
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
const result = await db.query(
|
|
844
|
+
'INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *',
|
|
845
|
+
[name, email]
|
|
846
|
+
);
|
|
847
|
+
res.status(201).json(result.rows[0]);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
res.status(500).json({ error: error.message });
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
// Graceful shutdown
|
|
854
|
+
process.on('SIGTERM', async () => {
|
|
855
|
+
await db.disconnect();
|
|
856
|
+
process.exit(0);
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
app.listen(3000, () => {
|
|
860
|
+
console.log('Server running on port 3000');
|
|
861
|
+
});
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
## Why pg-light?
|
|
865
|
+
|
|
866
|
+
- โ
**Minimal Learning Curve** - One function to connect, simple API
|
|
867
|
+
- โ
**Production Ready** - Built on battle-tested `pg` library
|
|
868
|
+
- โ
**Clean Code** - All functions โค7 lines, easy to maintain
|
|
869
|
+
- โ
**Integrated Logging** - Uses `logger-standard` for consistent logging
|
|
870
|
+
- โ
**Transaction Safety** - Auto-rollback on errors
|
|
871
|
+
- โ
**Connection Pooling** - Efficient resource management
|
|
872
|
+
- โ
**Parameterized Queries** - SQL injection protection
|
|
873
|
+
- โ
**Zero Magic** - Straightforward, predictable behavior
|
|
874
|
+
|
|
875
|
+
## Comparison
|
|
876
|
+
|
|
877
|
+
### Before (pg directly)
|
|
878
|
+
|
|
879
|
+
```js
|
|
880
|
+
import pg from 'pg';
|
|
881
|
+
|
|
882
|
+
const pool = new pg.Pool({
|
|
883
|
+
connectionString: process.env.DATABASE_URL
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
pool.on('error', (err) => {
|
|
887
|
+
console.error('Unexpected error', err);
|
|
888
|
+
process.exit(-1);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const client = await pool.connect();
|
|
892
|
+
try {
|
|
893
|
+
const result = await client.query('SELECT * FROM users');
|
|
894
|
+
console.log(result.rows);
|
|
895
|
+
} finally {
|
|
896
|
+
client.release();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
await pool.end();
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### After (pg-light)
|
|
903
|
+
|
|
904
|
+
```js
|
|
905
|
+
import { connectDB } from '@entergreat/pg-light';
|
|
906
|
+
|
|
907
|
+
const db = await connectDB(process.env.DATABASE_URL);
|
|
908
|
+
const result = await db.query('SELECT * FROM users');
|
|
909
|
+
console.log(result.rows);
|
|
910
|
+
await db.disconnect();
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
## License
|
|
914
|
+
|
|
915
|
+
ISC
|
|
916
|
+
|
|
917
|
+
## Contributing
|
|
918
|
+
|
|
919
|
+
Contributions, issues, and feature requests are welcome!
|
|
920
|
+
|
|
921
|
+
## Related Packages
|
|
922
|
+
|
|
923
|
+
- [logger-standard](https://www.npmjs.com/package/logger-standard) - Flexible logging utility (used internally)
|
|
924
|
+
- [pg](https://www.npmjs.com/package/pg) - PostgreSQL client (underlying library)
|
|
925
|
+
|
|
926
|
+
## Repository
|
|
927
|
+
|
|
928
|
+
[https://github.com/nivkman/pg-light](https://github.com/nivkman/pg-light)
|