@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/example-knex.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { connectDB } from './src/index.js';
|
|
2
|
+
|
|
3
|
+
// Example: Using Knex Query Builder for Complex Queries
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
// Connect to database
|
|
7
|
+
const db = await connectDB('postgresql://user:password@localhost:5432/mydb', {
|
|
8
|
+
service: 'knex-example',
|
|
9
|
+
logLevel: 'debug'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// SIMPLE QUERIES WITH KNEX
|
|
14
|
+
|
|
15
|
+
// Select all users
|
|
16
|
+
const allUsers = await db.builder('users').select('*');
|
|
17
|
+
console.log('All users:', allUsers);
|
|
18
|
+
|
|
19
|
+
// Select with WHERE
|
|
20
|
+
const activeUsers = await db.builder('users')
|
|
21
|
+
.select('*')
|
|
22
|
+
.where({ active: true })
|
|
23
|
+
.orderBy('created_at', 'desc');
|
|
24
|
+
console.log('Active users:', activeUsers);
|
|
25
|
+
|
|
26
|
+
// Select specific columns
|
|
27
|
+
const userEmails = await db.builder('users')
|
|
28
|
+
.select('id', 'name', 'email')
|
|
29
|
+
.where('age', '>', 25)
|
|
30
|
+
.limit(10);
|
|
31
|
+
console.log('User emails:', userEmails);
|
|
32
|
+
|
|
33
|
+
// COMPLEX QUERIES
|
|
34
|
+
|
|
35
|
+
// JOIN queries
|
|
36
|
+
const postsWithAuthors = await db.builder('posts')
|
|
37
|
+
.select('posts.*', 'users.name as author_name', 'users.email as author_email')
|
|
38
|
+
.join('users', 'posts.user_id', 'users.id')
|
|
39
|
+
.where('posts.published', true)
|
|
40
|
+
.orderBy('posts.created_at', 'desc')
|
|
41
|
+
.limit(20);
|
|
42
|
+
console.log('Posts with authors:', postsWithAuthors);
|
|
43
|
+
|
|
44
|
+
// Multiple JOINs
|
|
45
|
+
const postsWithCommentsCount = await db.builder('posts')
|
|
46
|
+
.select('posts.*', 'users.name as author')
|
|
47
|
+
.count('comments.id as comment_count')
|
|
48
|
+
.leftJoin('users', 'posts.user_id', 'users.id')
|
|
49
|
+
.leftJoin('comments', 'posts.id', 'comments.post_id')
|
|
50
|
+
.groupBy('posts.id', 'users.name')
|
|
51
|
+
.having(db.knex.raw('COUNT(comments.id) > ?', [5]))
|
|
52
|
+
.orderBy('comment_count', 'desc');
|
|
53
|
+
console.log('Popular posts:', postsWithCommentsCount);
|
|
54
|
+
|
|
55
|
+
// WHERE with multiple conditions
|
|
56
|
+
const filteredUsers = await db.builder('users')
|
|
57
|
+
.select('*')
|
|
58
|
+
.where('active', true)
|
|
59
|
+
.andWhere('age', '>=', 18)
|
|
60
|
+
.andWhere('age', '<=', 65)
|
|
61
|
+
.whereIn('role', ['admin', 'moderator'])
|
|
62
|
+
.whereNotNull('email')
|
|
63
|
+
.orderBy('created_at', 'desc');
|
|
64
|
+
console.log('Filtered users:', filteredUsers);
|
|
65
|
+
|
|
66
|
+
// OR conditions
|
|
67
|
+
const searchResults = await db.builder('users')
|
|
68
|
+
.select('*')
|
|
69
|
+
.where('name', 'like', '%John%')
|
|
70
|
+
.orWhere('email', 'like', '%john%')
|
|
71
|
+
.limit(10);
|
|
72
|
+
console.log('Search results:', searchResults);
|
|
73
|
+
|
|
74
|
+
// Subqueries
|
|
75
|
+
const usersWithManyPosts = await db.builder('users')
|
|
76
|
+
.select('users.*')
|
|
77
|
+
.whereIn('users.id', function() {
|
|
78
|
+
this.select('user_id')
|
|
79
|
+
.from('posts')
|
|
80
|
+
.groupBy('user_id')
|
|
81
|
+
.havingRaw('COUNT(*) > ?', [10]);
|
|
82
|
+
});
|
|
83
|
+
console.log('Prolific authors:', usersWithManyPosts);
|
|
84
|
+
|
|
85
|
+
// AGGREGATIONS
|
|
86
|
+
|
|
87
|
+
// Count
|
|
88
|
+
const userCount = await db.builder('users').count('* as total');
|
|
89
|
+
console.log('Total users:', userCount[0].total);
|
|
90
|
+
|
|
91
|
+
// Average, Sum, Min, Max
|
|
92
|
+
const stats = await db.builder('orders')
|
|
93
|
+
.select(
|
|
94
|
+
db.knex.raw('AVG(total) as avg_order'),
|
|
95
|
+
db.knex.raw('SUM(total) as revenue'),
|
|
96
|
+
db.knex.raw('MIN(total) as min_order'),
|
|
97
|
+
db.knex.raw('MAX(total) as max_order')
|
|
98
|
+
)
|
|
99
|
+
.where('status', 'completed');
|
|
100
|
+
console.log('Order stats:', stats[0]);
|
|
101
|
+
|
|
102
|
+
// Group by with aggregation
|
|
103
|
+
const ordersByUser = await db.builder('orders')
|
|
104
|
+
.select('user_id')
|
|
105
|
+
.count('* as order_count')
|
|
106
|
+
.sum('total as total_spent')
|
|
107
|
+
.groupBy('user_id')
|
|
108
|
+
.orderBy('total_spent', 'desc')
|
|
109
|
+
.limit(10);
|
|
110
|
+
console.log('Top spenders:', ordersByUser);
|
|
111
|
+
|
|
112
|
+
// INSERT
|
|
113
|
+
|
|
114
|
+
const [newUser] = await db.builder('users')
|
|
115
|
+
.insert({
|
|
116
|
+
name: 'Alice Johnson',
|
|
117
|
+
email: 'alice@example.com',
|
|
118
|
+
age: 28,
|
|
119
|
+
active: true
|
|
120
|
+
})
|
|
121
|
+
.returning('*');
|
|
122
|
+
console.log('Created user:', newUser);
|
|
123
|
+
|
|
124
|
+
// Bulk insert
|
|
125
|
+
const newUsers = await db.builder('users')
|
|
126
|
+
.insert([
|
|
127
|
+
{ name: 'Bob', email: 'bob@example.com', age: 30 },
|
|
128
|
+
{ name: 'Charlie', email: 'charlie@example.com', age: 25 }
|
|
129
|
+
])
|
|
130
|
+
.returning('*');
|
|
131
|
+
console.log('Created users:', newUsers);
|
|
132
|
+
|
|
133
|
+
// UPDATE
|
|
134
|
+
|
|
135
|
+
const updatedCount = await db.builder('users')
|
|
136
|
+
.where({ email: 'alice@example.com' })
|
|
137
|
+
.update({
|
|
138
|
+
last_login: db.knex.fn.now(),
|
|
139
|
+
login_count: db.knex.raw('login_count + 1')
|
|
140
|
+
});
|
|
141
|
+
console.log('Updated rows:', updatedCount);
|
|
142
|
+
|
|
143
|
+
// UPDATE with JOIN (PostgreSQL specific)
|
|
144
|
+
await db.knex.raw(`
|
|
145
|
+
UPDATE posts
|
|
146
|
+
SET featured = true
|
|
147
|
+
FROM users
|
|
148
|
+
WHERE posts.user_id = users.id
|
|
149
|
+
AND users.role = 'premium'
|
|
150
|
+
`);
|
|
151
|
+
|
|
152
|
+
// DELETE
|
|
153
|
+
|
|
154
|
+
const deletedCount = await db.builder('users')
|
|
155
|
+
.where('active', false)
|
|
156
|
+
.andWhere('last_login', '<', db.knex.raw("NOW() - INTERVAL '1 year'"))
|
|
157
|
+
.delete();
|
|
158
|
+
console.log('Deleted inactive users:', deletedCount);
|
|
159
|
+
|
|
160
|
+
// TRANSACTIONS WITH KNEX
|
|
161
|
+
|
|
162
|
+
await db.knex.transaction(async (trx) => {
|
|
163
|
+
// Insert user
|
|
164
|
+
const [user] = await trx('users')
|
|
165
|
+
.insert({ name: 'David', email: 'david@example.com' })
|
|
166
|
+
.returning('*');
|
|
167
|
+
|
|
168
|
+
// Insert posts for user
|
|
169
|
+
await trx('posts').insert([
|
|
170
|
+
{ user_id: user.id, title: 'Post 1', content: 'Content 1' },
|
|
171
|
+
{ user_id: user.id, title: 'Post 2', content: 'Content 2' }
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
// Update user stats
|
|
175
|
+
await trx('users')
|
|
176
|
+
.where({ id: user.id })
|
|
177
|
+
.update({ post_count: 2 });
|
|
178
|
+
|
|
179
|
+
console.log('Transaction completed');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// RAW QUERIES WITH KNEX
|
|
183
|
+
|
|
184
|
+
const customQuery = await db.knex.raw(`
|
|
185
|
+
SELECT u.*, COUNT(p.id) as post_count
|
|
186
|
+
FROM users u
|
|
187
|
+
LEFT JOIN posts p ON u.id = p.user_id
|
|
188
|
+
WHERE u.active = ?
|
|
189
|
+
GROUP BY u.id
|
|
190
|
+
HAVING COUNT(p.id) > ?
|
|
191
|
+
ORDER BY post_count DESC
|
|
192
|
+
LIMIT ?
|
|
193
|
+
`, [true, 5, 10]);
|
|
194
|
+
console.log('Custom query results:', customQuery.rows);
|
|
195
|
+
|
|
196
|
+
// COMBINING MODEL API WITH KNEX
|
|
197
|
+
|
|
198
|
+
const User = db.model('users');
|
|
199
|
+
|
|
200
|
+
// Simple CRUD with Model
|
|
201
|
+
const simpleUser = await User.findById(1);
|
|
202
|
+
console.log('Simple find:', simpleUser);
|
|
203
|
+
|
|
204
|
+
// Complex query with Knex
|
|
205
|
+
const complexQuery = await db.builder('users')
|
|
206
|
+
.select('users.*', db.knex.raw('COUNT(posts.id) as post_count'))
|
|
207
|
+
.leftJoin('posts', 'users.id', 'posts.user_id')
|
|
208
|
+
.where('users.active', true)
|
|
209
|
+
.groupBy('users.id')
|
|
210
|
+
.having(db.knex.raw('COUNT(posts.id) > ?'), 10)
|
|
211
|
+
.orderBy('post_count', 'desc');
|
|
212
|
+
console.log('Complex query:', complexQuery);
|
|
213
|
+
|
|
214
|
+
// PAGINATION
|
|
215
|
+
|
|
216
|
+
const page = 2;
|
|
217
|
+
const perPage = 10;
|
|
218
|
+
const offset = (page - 1) * perPage;
|
|
219
|
+
|
|
220
|
+
const paginatedResults = await db.builder('users')
|
|
221
|
+
.select('*')
|
|
222
|
+
.where({ active: true })
|
|
223
|
+
.orderBy('created_at', 'desc')
|
|
224
|
+
.limit(perPage)
|
|
225
|
+
.offset(offset);
|
|
226
|
+
|
|
227
|
+
const [{ total }] = await db.builder('users')
|
|
228
|
+
.count('* as total')
|
|
229
|
+
.where({ active: true });
|
|
230
|
+
|
|
231
|
+
console.log({
|
|
232
|
+
data: paginatedResults,
|
|
233
|
+
page,
|
|
234
|
+
perPage,
|
|
235
|
+
total,
|
|
236
|
+
totalPages: Math.ceil(total / perPage)
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('Error:', error.message);
|
|
241
|
+
} finally {
|
|
242
|
+
await db.disconnect();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
main();
|
package/example-model.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { connectDB } from './src/index.js';
|
|
2
|
+
|
|
3
|
+
// Example: Mongoose-like Model API for pg-light
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
// Connect to database
|
|
7
|
+
const db = await connectDB('postgresql://user:password@localhost:5432/mydb', {
|
|
8
|
+
service: 'model-example',
|
|
9
|
+
logLevel: 'debug'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
// Create models (like Mongoose)
|
|
13
|
+
const User = db.model('users');
|
|
14
|
+
const Post = db.model('posts');
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
// CREATE - Insert new records
|
|
18
|
+
const newUser = await User.create({
|
|
19
|
+
name: 'John Doe',
|
|
20
|
+
email: 'john@example.com',
|
|
21
|
+
age: 30
|
|
22
|
+
});
|
|
23
|
+
console.log('Created user:', newUser);
|
|
24
|
+
|
|
25
|
+
// FIND ALL - Get all records
|
|
26
|
+
const allUsers = await User.find();
|
|
27
|
+
console.log('All users:', allUsers);
|
|
28
|
+
|
|
29
|
+
// FIND WITH CONDITIONS - Filter records
|
|
30
|
+
const activeUsers = await User.find({ active: true });
|
|
31
|
+
console.log('Active users:', activeUsers);
|
|
32
|
+
|
|
33
|
+
// FIND WITH OPTIONS - Pagination
|
|
34
|
+
const firstTenUsers = await User.find({}, { limit: 10, offset: 0 });
|
|
35
|
+
console.log('First 10 users:', firstTenUsers);
|
|
36
|
+
|
|
37
|
+
// FIND ONE - Get single record
|
|
38
|
+
const john = await User.findOne({ email: 'john@example.com' });
|
|
39
|
+
console.log('Found John:', john);
|
|
40
|
+
|
|
41
|
+
// FIND BY ID - Get by primary key
|
|
42
|
+
const userById = await User.findById(1);
|
|
43
|
+
console.log('User #1:', userById);
|
|
44
|
+
|
|
45
|
+
// UPDATE - Update records
|
|
46
|
+
const updatedUsers = await User.update(
|
|
47
|
+
{ email: 'john@example.com' },
|
|
48
|
+
{ last_login: new Date() }
|
|
49
|
+
);
|
|
50
|
+
console.log('Updated users:', updatedUsers);
|
|
51
|
+
|
|
52
|
+
// UPDATE BY ID - Update single record
|
|
53
|
+
const updatedUser = await User.updateById(1, { name: 'John Smith' });
|
|
54
|
+
console.log('Updated user:', updatedUser);
|
|
55
|
+
|
|
56
|
+
// COUNT - Count records
|
|
57
|
+
const totalUsers = await User.count();
|
|
58
|
+
console.log('Total users:', totalUsers);
|
|
59
|
+
|
|
60
|
+
const activeCount = await User.count({ active: true });
|
|
61
|
+
console.log('Active users count:', activeCount);
|
|
62
|
+
|
|
63
|
+
// DELETE - Remove records
|
|
64
|
+
const deletedUsers = await User.delete({ active: false });
|
|
65
|
+
console.log('Deleted inactive users:', deletedUsers);
|
|
66
|
+
|
|
67
|
+
// DELETE BY ID - Remove single record
|
|
68
|
+
const deletedUser = await User.deleteById(999);
|
|
69
|
+
console.log('Deleted user:', deletedUser);
|
|
70
|
+
|
|
71
|
+
// RELATIONSHIPS - Create related records
|
|
72
|
+
const post = await Post.create({
|
|
73
|
+
title: 'My First Post',
|
|
74
|
+
content: 'Hello World!',
|
|
75
|
+
user_id: newUser.id,
|
|
76
|
+
published: true
|
|
77
|
+
});
|
|
78
|
+
console.log('Created post:', post);
|
|
79
|
+
|
|
80
|
+
// Find posts by user
|
|
81
|
+
const userPosts = await Post.find({ user_id: newUser.id });
|
|
82
|
+
console.log('User posts:', userPosts);
|
|
83
|
+
|
|
84
|
+
// COMPLEX QUERIES - Still use raw SQL when needed
|
|
85
|
+
const popularPosts = await db.query(
|
|
86
|
+
`SELECT p.*, u.name as author_name
|
|
87
|
+
FROM posts p
|
|
88
|
+
JOIN users u ON p.user_id = u.id
|
|
89
|
+
WHERE p.views > $1
|
|
90
|
+
ORDER BY p.views DESC
|
|
91
|
+
LIMIT $2`,
|
|
92
|
+
[1000, 10]
|
|
93
|
+
);
|
|
94
|
+
console.log('Popular posts:', popularPosts.rows);
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('Error:', error.message);
|
|
98
|
+
} finally {
|
|
99
|
+
await db.disconnect();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
main();
|
package/example.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { connectDB } from './src/index.js';
|
|
2
|
+
|
|
3
|
+
// Example usage of pg-light
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
// Connect to database (use your connection string)
|
|
7
|
+
const db = await connectDB('postgresql://user:password@localhost:5432/mydb', {
|
|
8
|
+
service: 'pg-light-example',
|
|
9
|
+
logLevel: 'debug'
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
// Simple query
|
|
14
|
+
const result = await db.query('SELECT NOW() as current_time');
|
|
15
|
+
console.log('Current time:', result.rows[0].current_time);
|
|
16
|
+
|
|
17
|
+
// Parameterized query
|
|
18
|
+
const users = await db.query(
|
|
19
|
+
'SELECT * FROM users WHERE active = $1 LIMIT $2',
|
|
20
|
+
[true, 10]
|
|
21
|
+
);
|
|
22
|
+
console.log('Active users:', users.rows);
|
|
23
|
+
|
|
24
|
+
// Transaction example
|
|
25
|
+
await db.transaction(async (client) => {
|
|
26
|
+
await client.query('INSERT INTO logs (message) VALUES ($1)', ['Transaction started']);
|
|
27
|
+
await client.query('INSERT INTO logs (message) VALUES ($1)', ['Transaction completed']);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
} catch (error) {
|
|
31
|
+
console.error('Error:', error.message);
|
|
32
|
+
} finally {
|
|
33
|
+
// Always disconnect
|
|
34
|
+
await db.disconnect();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@entergreat/pg-light",
|
|
3
|
+
"description": "Lightweight PostgreSQL client with Mongoose-like models. One-line connection, friendly CRUD API, and raw SQL support.",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/nivkman/pg-light.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"postgresql",
|
|
16
|
+
"postgres",
|
|
17
|
+
"pg",
|
|
18
|
+
"database",
|
|
19
|
+
"connection",
|
|
20
|
+
"pool",
|
|
21
|
+
"lightweight",
|
|
22
|
+
"simple",
|
|
23
|
+
"one-liner",
|
|
24
|
+
"sql",
|
|
25
|
+
"query",
|
|
26
|
+
"node",
|
|
27
|
+
"nodejs",
|
|
28
|
+
"model",
|
|
29
|
+
"mongoose",
|
|
30
|
+
"orm",
|
|
31
|
+
"crud",
|
|
32
|
+
"activerecord"
|
|
33
|
+
],
|
|
34
|
+
"author": "EnterGreat",
|
|
35
|
+
"license": "ISC",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/nivkman/pg-light/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/nivkman/pg-light#readme",
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"pg": "^8.11.3",
|
|
42
|
+
"knex": "^3.1.0",
|
|
43
|
+
"logger-standard": "^2.0.0"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18.0.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import knex from 'knex';
|
|
3
|
+
import { Logger } from 'logger-standard';
|
|
4
|
+
import { Model } from './model.js';
|
|
5
|
+
|
|
6
|
+
const { Pool } = pg;
|
|
7
|
+
|
|
8
|
+
function isConnectionString(config) {
|
|
9
|
+
return typeof config === 'string';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function createPoolConfig(config) {
|
|
13
|
+
if (isConnectionString(config)) {
|
|
14
|
+
return { connectionString: config };
|
|
15
|
+
}
|
|
16
|
+
return config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createLogger(options) {
|
|
20
|
+
const service = options?.service || 'pg-light';
|
|
21
|
+
const logLevel = options?.logLevel || 'info';
|
|
22
|
+
return new Logger({ service, logLevel });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function createKnexInstance(config) {
|
|
26
|
+
const connection = isConnectionString(config) ? config : createPoolConfig(config);
|
|
27
|
+
return knex({ client: 'pg', connection });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class PgLight {
|
|
31
|
+
#pool = null;
|
|
32
|
+
#logger = null;
|
|
33
|
+
#knex = null;
|
|
34
|
+
_logger = null;
|
|
35
|
+
|
|
36
|
+
constructor(pool, logger, knexInstance) {
|
|
37
|
+
this.#pool = pool;
|
|
38
|
+
this.#logger = logger;
|
|
39
|
+
this.#knex = knexInstance;
|
|
40
|
+
this._logger = logger;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async query(text, params = []) {
|
|
44
|
+
this.#logger.debug(`Executing query: ${text}`);
|
|
45
|
+
const result = await this.#pool.query(text, params);
|
|
46
|
+
this.#logger.debug(`Query returned ${result.rowCount} rows`);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async #executeTransaction(client, callback) {
|
|
51
|
+
await client.query('BEGIN');
|
|
52
|
+
await callback(client);
|
|
53
|
+
await client.query('COMMIT');
|
|
54
|
+
this.#logger.debug('Transaction committed');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async #rollbackTransaction(client, error) {
|
|
58
|
+
await client.query('ROLLBACK');
|
|
59
|
+
this.#logger.error(`Transaction rolled back: ${error.message}`);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async transaction(callback) {
|
|
64
|
+
const client = await this.#pool.connect();
|
|
65
|
+
try {
|
|
66
|
+
await this.#executeTransaction(client, callback);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
await this.#rollbackTransaction(client, error);
|
|
69
|
+
} finally {
|
|
70
|
+
client.release();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async disconnect() {
|
|
75
|
+
await this.#pool.end();
|
|
76
|
+
await this.#knex.destroy();
|
|
77
|
+
this.#logger.info('Database connection closed');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getPool() {
|
|
81
|
+
return this.#pool;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
model(tableName) {
|
|
85
|
+
return new Model(this, tableName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
builder(tableName) {
|
|
89
|
+
this.#logger.debug(`Creating query builder for ${tableName}`);
|
|
90
|
+
return this.#knex(tableName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get knex() {
|
|
94
|
+
return this.#knex;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function testConnection(pool, logger) {
|
|
99
|
+
try {
|
|
100
|
+
await pool.query('SELECT NOW()');
|
|
101
|
+
logger.info('Database connected successfully');
|
|
102
|
+
} catch (error) {
|
|
103
|
+
logger.error(`Connection failed: ${error.message}`);
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function connectDB(config, options = {}) {
|
|
109
|
+
const logger = createLogger(options);
|
|
110
|
+
const poolConfig = createPoolConfig(config);
|
|
111
|
+
const pool = new Pool(poolConfig);
|
|
112
|
+
const knexInstance = createKnexInstance(config);
|
|
113
|
+
await testConnection(pool, logger);
|
|
114
|
+
return new PgLight(pool, logger, knexInstance);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { Model };
|
package/src/model.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
function buildWhereClause(conditions) {
|
|
2
|
+
const keys = Object.keys(conditions);
|
|
3
|
+
const whereParts = keys.map((key, idx) => `${key} = $${idx + 1}`);
|
|
4
|
+
const values = keys.map(key => conditions[key]);
|
|
5
|
+
return { clause: whereParts.join(' AND '), values };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createPlaceholders(keys) {
|
|
9
|
+
return keys.map((_, idx) => `$${idx + 1}`).join(', ');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getKeysAndValues(data) {
|
|
13
|
+
const keys = Object.keys(data);
|
|
14
|
+
const values = keys.map(key => data[key]);
|
|
15
|
+
return { keys, values };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildInsertQuery(tableName, data) {
|
|
19
|
+
const { keys, values } = getKeysAndValues(data);
|
|
20
|
+
const columns = keys.join(', ');
|
|
21
|
+
const placeholders = createPlaceholders(keys);
|
|
22
|
+
const text = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
|
|
23
|
+
return { text, values };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildSetClause(data) {
|
|
27
|
+
const keys = Object.keys(data);
|
|
28
|
+
const setParts = keys.map((key, idx) => `${key} = $${idx + 1}`);
|
|
29
|
+
const values = keys.map(key => data[key]);
|
|
30
|
+
return { clause: setParts.join(', '), values };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildUpdateQuery(tableName, data, conditions) {
|
|
34
|
+
const { clause: setClause, values: setValues } = buildSetClause(data);
|
|
35
|
+
const { clause: whereClause, values: whereValues } = buildWhereClause(conditions);
|
|
36
|
+
const text = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`;
|
|
37
|
+
const values = [...setValues, ...whereValues];
|
|
38
|
+
return { text, values };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class Model {
|
|
42
|
+
#db = null;
|
|
43
|
+
#tableName = null;
|
|
44
|
+
#logger = null;
|
|
45
|
+
|
|
46
|
+
constructor(db, tableName) {
|
|
47
|
+
this.#db = db;
|
|
48
|
+
this.#tableName = tableName;
|
|
49
|
+
this.#logger = db._logger;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async find(conditions = {}, options = {}) {
|
|
53
|
+
const hasConditions = Object.keys(conditions).length > 0;
|
|
54
|
+
|
|
55
|
+
if (!hasConditions) {
|
|
56
|
+
return this.#findAll(options);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return this.#findWithConditions(conditions, options);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async #findAll(options) {
|
|
63
|
+
const limit = options.limit ? `LIMIT ${options.limit}` : '';
|
|
64
|
+
const offset = options.offset ? `OFFSET ${options.offset}` : '';
|
|
65
|
+
const query = `SELECT * FROM ${this.#tableName} ${limit} ${offset}`.trim();
|
|
66
|
+
const result = await this.#db.query(query);
|
|
67
|
+
return result.rows;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async #findWithConditions(conditions, options) {
|
|
71
|
+
const { clause, values } = buildWhereClause(conditions);
|
|
72
|
+
const limit = options.limit ? `LIMIT ${options.limit}` : '';
|
|
73
|
+
const offset = options.offset ? `OFFSET ${options.offset}` : '';
|
|
74
|
+
const query = `SELECT * FROM ${this.#tableName} WHERE ${clause} ${limit} ${offset}`.trim();
|
|
75
|
+
const result = await this.#db.query(query, values);
|
|
76
|
+
return result.rows;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async findOne(conditions) {
|
|
80
|
+
const rows = await this.find(conditions, { limit: 1 });
|
|
81
|
+
return rows[0] || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async findById(id) {
|
|
85
|
+
return this.findOne({ id });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async create(data) {
|
|
89
|
+
const { text, values } = buildInsertQuery(this.#tableName, data);
|
|
90
|
+
const result = await this.#db.query(text, values);
|
|
91
|
+
this.#logger.debug(`Created record in ${this.#tableName}`);
|
|
92
|
+
return result.rows[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async update(conditions, data) {
|
|
96
|
+
const { text, values } = buildUpdateQuery(this.#tableName, data, conditions);
|
|
97
|
+
const result = await this.#db.query(text, values);
|
|
98
|
+
this.#logger.debug(`Updated ${result.rowCount} rows in ${this.#tableName}`);
|
|
99
|
+
return result.rows;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async updateById(id, data) {
|
|
103
|
+
const result = await this.update({ id }, data);
|
|
104
|
+
return result[0] || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async delete(conditions) {
|
|
108
|
+
const { clause, values } = buildWhereClause(conditions);
|
|
109
|
+
const query = `DELETE FROM ${this.#tableName} WHERE ${clause} RETURNING *`;
|
|
110
|
+
const result = await this.#db.query(query, values);
|
|
111
|
+
this.#logger.debug(`Deleted ${result.rowCount} rows from ${this.#tableName}`);
|
|
112
|
+
return result.rows;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async deleteById(id) {
|
|
116
|
+
const result = await this.delete({ id });
|
|
117
|
+
return result[0] || null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async #countAll() {
|
|
121
|
+
const result = await this.#db.query(`SELECT COUNT(*) FROM ${this.#tableName}`);
|
|
122
|
+
return parseInt(result.rows[0].count);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async #countWithConditions(conditions) {
|
|
126
|
+
const { clause, values } = buildWhereClause(conditions);
|
|
127
|
+
const query = `SELECT COUNT(*) FROM ${this.#tableName} WHERE ${clause}`;
|
|
128
|
+
const result = await this.#db.query(query, values);
|
|
129
|
+
return parseInt(result.rows[0].count);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async count(conditions = {}) {
|
|
133
|
+
const hasConditions = Object.keys(conditions).length > 0;
|
|
134
|
+
if (!hasConditions) return this.#countAll();
|
|
135
|
+
return this.#countWithConditions(conditions);
|
|
136
|
+
}
|
|
137
|
+
}
|