neopg 1.0.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.cn.md +26 -7
- package/README.md +31 -14
- package/images/neopg-end.webp +0 -0
- package/lib/NeoPG.js +62 -0
- package/package.json +1 -1
- package/test/test-db.js +15 -0
- package/images/neopg-programming.jpeg +0 -0
package/README.cn.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
**NeoPG** 是一个基于 [postgres.js](https://github.com/porsager/postgres)(Node.js 生态中最快的 PostgreSQL 客户端)构建的高性能、零依赖 ORM。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
它完美地融合了链式查询构造器(Query Builder)带来的极佳开发体验(DX)与原生 SQL 模板字符串(Template Literals)的极致性能。
|
|
10
10
|
|
|
11
11
|
### [📃 English Document 🔗](./README.md)
|
|
12
12
|
|
|
@@ -212,6 +212,28 @@ console.log('数据库结构已同步!')
|
|
|
212
212
|
|
|
213
213
|
---
|
|
214
214
|
|
|
215
|
+
### 📂 自动加载模型
|
|
216
|
+
|
|
217
|
+
NeoPG 支持扫描指定目录并自动注册所有模型,无需手动逐个引入。
|
|
218
|
+
|
|
219
|
+
**加载规则:**
|
|
220
|
+
* 仅加载 `.js` 和 `.mjs` 后缀的文件。
|
|
221
|
+
* **忽略**以 `_` 开头的文件(可用作目录内的共享工具或基类)。
|
|
222
|
+
* **忽略**以 `!` 开头的文件(可用作临时禁用的模型)。
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
const db = new NeoPG(config)
|
|
226
|
+
|
|
227
|
+
// 自动加载 ./models 目录下的所有模型
|
|
228
|
+
// 注意:这是一个异步方法,因为它兼容 ESM (.mjs) 的动态导入
|
|
229
|
+
await db.loadModels('./models')
|
|
230
|
+
|
|
231
|
+
// 加载完成后即可同步或使用
|
|
232
|
+
await db.sync()
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
215
237
|
## 🔍 查询数据
|
|
216
238
|
|
|
217
239
|
NeoPG 提供了自然流畅的链式 API。
|
|
@@ -239,16 +261,13 @@ const page2 = await db.model('User').page(2, 20).find(); // 第 2 页,每页 2
|
|
|
239
261
|
|
|
240
262
|
```javascript
|
|
241
263
|
await db.model('User')
|
|
242
|
-
// 对象风格 (自动处理 AND)
|
|
243
264
|
.where({
|
|
244
265
|
age: 18,
|
|
245
266
|
status: 'active'
|
|
246
267
|
})
|
|
247
|
-
// 操作符风格
|
|
248
268
|
.where('create_time', '>', 1600000000)
|
|
249
|
-
// SQL 片段风格 (强大且灵活!)
|
|
250
269
|
.where('id IS NOT NULL')
|
|
251
|
-
.find()
|
|
270
|
+
.find()
|
|
252
271
|
```
|
|
253
272
|
|
|
254
273
|
### 结合模板字符串的复杂查询
|
|
@@ -259,9 +278,9 @@ await db.model('User')
|
|
|
259
278
|
// db.sql 是原生的 postgres 实例
|
|
260
279
|
const { sql } = db;
|
|
261
280
|
|
|
281
|
+
// 通过模板字符串安全地注入参数
|
|
262
282
|
await db.model('User')
|
|
263
283
|
.where({ status: 'active' })
|
|
264
|
-
// 通过模板字符串安全地注入参数
|
|
265
284
|
.where(sql`age > ${20} AND email LIKE ${'%@gmail.com'}`)
|
|
266
285
|
.find();
|
|
267
286
|
```
|
|
@@ -412,4 +431,4 @@ await db.sql.begin(async (sql) => {
|
|
|
412
431
|
|
|
413
432
|
ISC
|
|
414
433
|
|
|
415
|
-

|
package/README.md
CHANGED
|
@@ -211,6 +211,28 @@ console.log('Database synced!')
|
|
|
211
211
|
|
|
212
212
|
---
|
|
213
213
|
|
|
214
|
+
### 📂 Auto-loading Models
|
|
215
|
+
|
|
216
|
+
Instead of manually importing and defining each model, you can load all models from a directory.
|
|
217
|
+
|
|
218
|
+
**Rules:**
|
|
219
|
+
* Only `.js` and `.mjs` files are loaded.
|
|
220
|
+
* Files starting with `_` are ignored (useful for utils/helpers).
|
|
221
|
+
* Files starting with `!` are ignored (useful for disabled models).
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
const db = new NeoPG(config)
|
|
225
|
+
|
|
226
|
+
// Load all models from the './models' directory
|
|
227
|
+
// This is asynchronous because it supports .mjs dynamic imports
|
|
228
|
+
await db.loadModels('./models')
|
|
229
|
+
|
|
230
|
+
// Now you can sync and use them
|
|
231
|
+
await db.sync()
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
214
236
|
## 🔍 Querying
|
|
215
237
|
|
|
216
238
|
NeoPG provides a fluent, chainable API that feels natural to use.
|
|
@@ -238,16 +260,13 @@ const page2 = await db.model('User').page(2, 20).find(); // Page 2, Size 20
|
|
|
238
260
|
|
|
239
261
|
```javascript
|
|
240
262
|
await db.model('User')
|
|
241
|
-
// Object style (AND logic)
|
|
242
263
|
.where({
|
|
243
264
|
age: 18,
|
|
244
265
|
status: 'active'
|
|
245
266
|
})
|
|
246
|
-
// Operator style
|
|
247
267
|
.where('create_time', '>', 1600000000)
|
|
248
|
-
// SQL Fragment style (Powerful!)
|
|
249
268
|
.where('id IS NOT NULL')
|
|
250
|
-
.find()
|
|
269
|
+
.find()
|
|
251
270
|
```
|
|
252
271
|
|
|
253
272
|
### Complex Where with Template Literals
|
|
@@ -300,14 +319,14 @@ const stats = await db.model('User')
|
|
|
300
319
|
const newUser = await db.model('User').insert({
|
|
301
320
|
username: 'neo',
|
|
302
321
|
email: 'neo@matrix.com'
|
|
303
|
-
})
|
|
322
|
+
})
|
|
304
323
|
// ID and Timestamps are automatically generated if configured in Schema
|
|
305
324
|
|
|
306
325
|
// Insert multiple (Batch)
|
|
307
326
|
await db.model('User').insert([
|
|
308
327
|
{ username: 'a' },
|
|
309
328
|
{ username: 'b' }
|
|
310
|
-
])
|
|
329
|
+
])
|
|
311
330
|
```
|
|
312
331
|
|
|
313
332
|
### Update
|
|
@@ -384,16 +403,15 @@ const result = await db.transaction(async (tx) => {
|
|
|
384
403
|
const user = await tx.model('User').insert({ username: 'alice' });
|
|
385
404
|
|
|
386
405
|
// 2. Read operation within transaction
|
|
387
|
-
const count = await tx.model('User').count()
|
|
406
|
+
const count = await tx.model('User').count()
|
|
388
407
|
|
|
389
408
|
// 3. Throwing an error will automatically ROLLBACK
|
|
390
409
|
if (count > 100) {
|
|
391
|
-
throw new Error('Limit reached')
|
|
410
|
+
throw new Error('Limit reached')
|
|
392
411
|
}
|
|
393
412
|
|
|
394
|
-
return user
|
|
395
|
-
})
|
|
396
|
-
// Automatically COMMITTED here if no error
|
|
413
|
+
return user
|
|
414
|
+
})
|
|
397
415
|
```
|
|
398
416
|
|
|
399
417
|
### Using Raw Postgres Transaction
|
|
@@ -402,7 +420,7 @@ const result = await db.transaction(async (tx) => {
|
|
|
402
420
|
await db.sql.begin(async (sql) => {
|
|
403
421
|
// sql is the transaction connection
|
|
404
422
|
await sql`INSERT INTO users (name) VALUES ('bob')`;
|
|
405
|
-
})
|
|
423
|
+
})
|
|
406
424
|
```
|
|
407
425
|
|
|
408
426
|
---
|
|
@@ -411,5 +429,4 @@ await db.sql.begin(async (sql) => {
|
|
|
411
429
|
|
|
412
430
|
ISC
|
|
413
431
|
|
|
414
|
-
|
|
415
|
-

|
|
432
|
+

|
|
Binary file
|
package/lib/NeoPG.js
CHANGED
|
@@ -7,6 +7,10 @@ const TransactionScope = require('./TransactionScope.js')
|
|
|
7
7
|
const ModelChain = require('./ModelChain.js')
|
|
8
8
|
const dataTypes = require('./dataTypes.js')
|
|
9
9
|
|
|
10
|
+
const path = require('node:path')
|
|
11
|
+
const fs = require('node:fs')
|
|
12
|
+
const { pathToFileURL } = require('node:url')
|
|
13
|
+
|
|
10
14
|
class NeoPG {
|
|
11
15
|
constructor(config) {
|
|
12
16
|
this.driver = postgres(config)
|
|
@@ -136,6 +140,64 @@ class NeoPG {
|
|
|
136
140
|
async close() {
|
|
137
141
|
await this.driver.end()
|
|
138
142
|
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 自动加载指定目录下的模型文件
|
|
146
|
+
* 规则:
|
|
147
|
+
* 1. 只加载 .js 和 .mjs 文件
|
|
148
|
+
* 2. 跳过以 '_' 开头的文件 (视为内部共享模块)
|
|
149
|
+
* 3. 跳过以 '!' 开头的文件 (视为保留或禁用文件)
|
|
150
|
+
*
|
|
151
|
+
* @param {string} dirPath - 目录路径 (相对 process.cwd() 或绝对路径)
|
|
152
|
+
*/
|
|
153
|
+
async loadModels(dirPath) {
|
|
154
|
+
// 解析绝对路径
|
|
155
|
+
const absPath = path.isAbsolute(dirPath)
|
|
156
|
+
? dirPath
|
|
157
|
+
: path.resolve(process.cwd(), dirPath)
|
|
158
|
+
|
|
159
|
+
if (!fs.existsSync(absPath)) {
|
|
160
|
+
throw new Error(`[NeoPG] Models directory not found: ${absPath}`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const files = fs.readdirSync(absPath)
|
|
164
|
+
|
|
165
|
+
for (const file of files) {
|
|
166
|
+
// 过滤规则
|
|
167
|
+
if (file.startsWith('_') || file.startsWith('!')) continue
|
|
168
|
+
|
|
169
|
+
const ext = path.extname(file)
|
|
170
|
+
|
|
171
|
+
if (ext !== '.js' && ext !== '.mjs') continue
|
|
172
|
+
|
|
173
|
+
const fullFilePath = path.join(absPath, file)
|
|
174
|
+
let modelExport
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
if (ext === '.mjs') {
|
|
178
|
+
// 处理 ESM 动态导入
|
|
179
|
+
// Windows 下 import() 需要 file:// 协议路径
|
|
180
|
+
const fileUrl = pathToFileURL(fullFilePath).href
|
|
181
|
+
const imported = await import(fileUrl)
|
|
182
|
+
modelExport = imported.default
|
|
183
|
+
} else {
|
|
184
|
+
// 处理 CommonJS
|
|
185
|
+
modelExport = require(fullFilePath)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 注册模型 (如果导出为空或不是合法对象/类,add 方法内部会抛错或处理)
|
|
189
|
+
if (modelExport) {
|
|
190
|
+
this.add(modelExport)
|
|
191
|
+
}
|
|
192
|
+
} catch (err) {
|
|
193
|
+
console.error(`[NeoPG] Failed to load model: ${file}`)
|
|
194
|
+
throw err
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return this
|
|
199
|
+
}
|
|
200
|
+
|
|
139
201
|
}
|
|
140
202
|
|
|
141
203
|
NeoPG.dataTypes = dataTypes
|
package/package.json
CHANGED
package/test/test-db.js
CHANGED
|
@@ -154,12 +154,21 @@ db.add(User);
|
|
|
154
154
|
{
|
|
155
155
|
username: 'Neo',
|
|
156
156
|
email: '123@w.com',
|
|
157
|
+
sex: 1,
|
|
157
158
|
level: Math.floor((Math.random() * 105))
|
|
158
159
|
},
|
|
159
160
|
{
|
|
160
161
|
username: 'PG',
|
|
161
162
|
email: '1234@w.com',
|
|
163
|
+
sex: 2,
|
|
162
164
|
level: Math.floor((Math.random() * 100))
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
{
|
|
168
|
+
username: 'NPG',
|
|
169
|
+
email: '1235@w.com',
|
|
170
|
+
sex: 3,
|
|
171
|
+
level: 3
|
|
163
172
|
}
|
|
164
173
|
])
|
|
165
174
|
)
|
|
@@ -179,6 +188,12 @@ db.add(User);
|
|
|
179
188
|
let result = await tx.model('User').where(tx.sql`level > 10`).returning('*').update(data)
|
|
180
189
|
console.log(result)
|
|
181
190
|
|
|
191
|
+
let sex = 3
|
|
192
|
+
console.log(
|
|
193
|
+
'test condition or',
|
|
194
|
+
await tx.model('User').where(tx.sql`(sex = ${sex} or level > 10)`).select(['id', 'level', 'username', 'sex']).find()
|
|
195
|
+
)
|
|
196
|
+
|
|
182
197
|
console.log(
|
|
183
198
|
'test avg',
|
|
184
199
|
await tx.model('User').avg('level')
|
|
Binary file
|