neopg 1.0.0 → 2.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.cn.md +113 -15
- package/README.md +120 -23
- package/bin/neopg-model.js +207 -0
- package/lib/NeoPG.js +73 -1
- package/package.json +4 -1
- package/test/test-db.js +15 -0
package/README.cn.md
CHANGED
|
@@ -64,7 +64,7 @@ await db.close();
|
|
|
64
64
|
创建一个模型文件(例如 `models/User.js`)。您的类应当继承自 `NeoPG.ModelChain`。
|
|
65
65
|
|
|
66
66
|
```javascript
|
|
67
|
-
const { ModelChain, dataTypes } = require('neopg')
|
|
67
|
+
const { ModelChain, dataTypes } = require('neopg')
|
|
68
68
|
|
|
69
69
|
class User extends ModelChain {
|
|
70
70
|
static schema = {
|
|
@@ -106,29 +106,130 @@ class User extends ModelChain {
|
|
|
106
106
|
index: ['email', 'age'],
|
|
107
107
|
// 唯一索引定义
|
|
108
108
|
unique: ['username']
|
|
109
|
-
}
|
|
109
|
+
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
module.exports = User
|
|
112
|
+
module.exports = User
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## 🛠 CLI 模型生成器
|
|
116
|
+
|
|
117
|
+
NeoPG 内置了一个 CLI 工具,可以快速生成带有样板代码的模型文件。
|
|
118
|
+
|
|
119
|
+
### 用法
|
|
120
|
+
|
|
121
|
+
通过 `npx` 直接运行(无需全局安装):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx neopg-model [选项] [模型名称...]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 选项
|
|
128
|
+
|
|
129
|
+
* `--dir=<path>`: 指定输出目录(默认:`./model`)。
|
|
130
|
+
|
|
131
|
+
### 示例
|
|
132
|
+
|
|
133
|
+
**1. 基础生成**
|
|
134
|
+
```bash
|
|
135
|
+
npx neopg-model user
|
|
136
|
+
# 创建文件: ./model/user.js
|
|
137
|
+
# 类名: User
|
|
138
|
+
# 表名: user
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**2. 命名规范(连字符处理)**
|
|
142
|
+
输入带连字符的名称,NeoPG 会自动将类名转换为 **大驼峰(CamelCase)**,将表名转换为 **下划线(snake_case)**。
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npx neopg-model user-log
|
|
146
|
+
# 创建文件: ./model/user-log.js
|
|
147
|
+
# 类名: UserLog
|
|
148
|
+
# 表名: user_log
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**3. 批量生成与自定义目录**
|
|
152
|
+
```bash
|
|
153
|
+
npx neopg-model --dir=./src/models product order-item
|
|
154
|
+
# 创建:
|
|
155
|
+
# ./src/models/product.js
|
|
156
|
+
# ./src/models/order-item.js
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**4. ES Modules (.mjs)**
|
|
160
|
+
如果在名称后加上 `.mjs` 后缀,将生成 ESM 语法(`export default`)的文件。
|
|
161
|
+
```bash
|
|
162
|
+
npx neopg-model config.mjs
|
|
113
163
|
```
|
|
114
164
|
|
|
115
165
|
---
|
|
116
166
|
|
|
117
167
|
## ⚙️ 注册与同步
|
|
118
168
|
|
|
119
|
-
初始化 NeoPG
|
|
169
|
+
初始化 NeoPG 并注册您的模型。您可以使用类(Class)或配置对象(Object)来定义模型。
|
|
170
|
+
|
|
171
|
+
### 注册模型
|
|
172
|
+
|
|
173
|
+
NeoPG 提供了三种注册方法以应对不同场景:
|
|
174
|
+
|
|
175
|
+
* **`define(model)`**:标准注册方法。如果同名模型已存在,会抛出错误(`modelName conflict`),防止意外覆盖。
|
|
176
|
+
* **`add(model)`**:同 `define`,行为一致。
|
|
177
|
+
* **`set(model)`**:**强制覆盖/重置**。如果模型已存在,则更新其定义。适用于热重载或动态 Schema 场景。
|
|
120
178
|
|
|
121
179
|
```javascript
|
|
122
|
-
const User = require('./models/User')
|
|
180
|
+
const User = require('./models/User')
|
|
181
|
+
|
|
182
|
+
// 1. 标准注册 (安全模式)
|
|
183
|
+
// 如果 'User' 已经被注册过,此处会报错
|
|
184
|
+
db.define(User)
|
|
185
|
+
|
|
186
|
+
// 2. 强制覆盖 (重置模式)
|
|
187
|
+
// 即使 'User' 已存在,也会使用新的定义覆盖它
|
|
188
|
+
db.set(User)
|
|
189
|
+
|
|
190
|
+
// 3. 使用纯对象注册 (快速原型)
|
|
191
|
+
db.define({
|
|
192
|
+
tableName: 'logs',
|
|
193
|
+
column: {
|
|
194
|
+
message: 'string',
|
|
195
|
+
level: 'int'
|
|
196
|
+
}
|
|
197
|
+
})
|
|
123
198
|
|
|
124
|
-
|
|
125
|
-
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 同步数据库
|
|
202
|
+
|
|
203
|
+
根据已注册的模型同步数据库表结构。
|
|
126
204
|
|
|
127
|
-
|
|
205
|
+
```javascript
|
|
206
|
+
// 同步表结构 (DDL)
|
|
128
207
|
// options: { force: true } 开启强制模式,会删除 Schema 中未定义的字段,请谨慎使用
|
|
129
|
-
await db.sync({ force: false })
|
|
208
|
+
await db.sync({ force: false })
|
|
130
209
|
|
|
131
|
-
console.log('数据库结构已同步!')
|
|
210
|
+
console.log('数据库结构已同步!')
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
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()
|
|
132
233
|
```
|
|
133
234
|
|
|
134
235
|
---
|
|
@@ -160,16 +261,13 @@ const page2 = await db.model('User').page(2, 20).find(); // 第 2 页,每页 2
|
|
|
160
261
|
|
|
161
262
|
```javascript
|
|
162
263
|
await db.model('User')
|
|
163
|
-
// 对象风格 (自动处理 AND)
|
|
164
264
|
.where({
|
|
165
265
|
age: 18,
|
|
166
266
|
status: 'active'
|
|
167
267
|
})
|
|
168
|
-
// 操作符风格
|
|
169
268
|
.where('create_time', '>', 1600000000)
|
|
170
|
-
// SQL 片段风格 (强大且灵活!)
|
|
171
269
|
.where('id IS NOT NULL')
|
|
172
|
-
.find()
|
|
270
|
+
.find()
|
|
173
271
|
```
|
|
174
272
|
|
|
175
273
|
### 结合模板字符串的复杂查询
|
|
@@ -180,9 +278,9 @@ await db.model('User')
|
|
|
180
278
|
// db.sql 是原生的 postgres 实例
|
|
181
279
|
const { sql } = db;
|
|
182
280
|
|
|
281
|
+
// 通过模板字符串安全地注入参数
|
|
183
282
|
await db.model('User')
|
|
184
283
|
.where({ status: 'active' })
|
|
185
|
-
// 通过模板字符串安全地注入参数
|
|
186
284
|
.where(sql`age > ${20} AND email LIKE ${'%@gmail.com'}`)
|
|
187
285
|
.find();
|
|
188
286
|
```
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ const db = new NeoPG(config);
|
|
|
54
54
|
### Close Connection
|
|
55
55
|
|
|
56
56
|
```javascript
|
|
57
|
-
await db.close()
|
|
57
|
+
await db.close()
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
---
|
|
@@ -64,7 +64,7 @@ await db.close();
|
|
|
64
64
|
Create a model file (e.g., `models/User.js`). Your class should extend `NeoPG.ModelChain`.
|
|
65
65
|
|
|
66
66
|
```javascript
|
|
67
|
-
const { ModelChain, dataTypes } = require('neopg')
|
|
67
|
+
const { ModelChain, dataTypes } = require('neopg')
|
|
68
68
|
|
|
69
69
|
class User extends ModelChain {
|
|
70
70
|
static schema = {
|
|
@@ -105,29 +105,130 @@ class User extends ModelChain {
|
|
|
105
105
|
// Indexes
|
|
106
106
|
index: ['email', 'age'],
|
|
107
107
|
unique: ['username']
|
|
108
|
-
}
|
|
108
|
+
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
module.exports = User
|
|
111
|
+
module.exports = User
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 🛠 CLI Model Generator
|
|
115
|
+
|
|
116
|
+
NeoPG includes a built-in CLI tool to quickly generate model files with boilerplate code.
|
|
117
|
+
|
|
118
|
+
### Usage
|
|
119
|
+
|
|
120
|
+
Run via `npx` (no global installation required):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx neopg-model [options] [model_names...]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Options
|
|
127
|
+
|
|
128
|
+
* `--dir=<path>`: Specify the output directory (default: `./model`).
|
|
129
|
+
|
|
130
|
+
### Examples
|
|
131
|
+
|
|
132
|
+
**1. Basic Generation**
|
|
133
|
+
```bash
|
|
134
|
+
npx neopg-model user
|
|
135
|
+
# Creates: ./model/user.js
|
|
136
|
+
# Class: User
|
|
137
|
+
# Table: user
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**2. Naming Convention (Hyphenated)**
|
|
141
|
+
NeoPG automatically converts hyphenated names to **CamelCase** for the class and **snake_case** for the table.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npx neopg-model user-log
|
|
145
|
+
# Creates: ./model/user-log.js
|
|
146
|
+
# Class: UserLog
|
|
147
|
+
# Table: user_log
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**3. Multiple Models & Custom Directory**
|
|
151
|
+
```bash
|
|
152
|
+
npx neopg-model --dir=./src/models product order-item
|
|
153
|
+
# Creates:
|
|
154
|
+
# ./src/models/product.js
|
|
155
|
+
# ./src/models/order-item.js
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**4. ES Modules (.mjs)**
|
|
159
|
+
If you suffix the name with `.mjs`, it generates ESM syntax (`export default`).
|
|
160
|
+
```bash
|
|
161
|
+
npx neopg-model config.mjs
|
|
112
162
|
```
|
|
113
163
|
|
|
114
164
|
---
|
|
115
165
|
|
|
116
166
|
## ⚙️ Registration & Sync
|
|
117
167
|
|
|
118
|
-
Initialize NeoPG and register your models. You can
|
|
168
|
+
Initialize NeoPG and register your models. You can define models using classes or configuration objects.
|
|
169
|
+
|
|
170
|
+
### Registering Models
|
|
171
|
+
|
|
172
|
+
NeoPG provides three methods for registration:
|
|
173
|
+
|
|
174
|
+
* **`define(model)`**: The standard method. Throws an error if a model with the same name already exists.
|
|
175
|
+
* **`add(model)`**: Alias for `define`.
|
|
176
|
+
* **`set(model)`**: **Overwrites** the existing model if the name conflicts. Useful for hot-reloading or dynamic schema updates.
|
|
119
177
|
|
|
120
178
|
```javascript
|
|
121
|
-
const User = require('./models/User')
|
|
179
|
+
const User = require('./models/User')
|
|
180
|
+
|
|
181
|
+
// 1. Standard Registration (Safe)
|
|
182
|
+
// Will throw error: "[NeoPG] modelName conflict: User" if registered twice
|
|
183
|
+
db.define(User)
|
|
184
|
+
|
|
185
|
+
// 2. Force Overwrite (Reset)
|
|
186
|
+
// Updates the definition for 'User' even if it exists
|
|
187
|
+
db.set(User)
|
|
188
|
+
|
|
189
|
+
// 3. Register using a plain object (Quick prototype)
|
|
190
|
+
db.define({
|
|
191
|
+
tableName: 'logs',
|
|
192
|
+
column: {
|
|
193
|
+
message: 'string',
|
|
194
|
+
level: 'int'
|
|
195
|
+
}
|
|
196
|
+
})
|
|
122
197
|
|
|
123
|
-
|
|
124
|
-
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Syncing Database
|
|
201
|
+
|
|
202
|
+
Sync the table structure to the database based on registered models.
|
|
125
203
|
|
|
126
|
-
|
|
204
|
+
```javascript
|
|
205
|
+
// Sync Table Structure (DDL)
|
|
127
206
|
// options: { force: true } will drop columns not defined in schema
|
|
128
|
-
await db.sync({ force: false })
|
|
207
|
+
await db.sync({ force: false })
|
|
129
208
|
|
|
130
|
-
console.log('Database synced!')
|
|
209
|
+
console.log('Database synced!')
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
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()
|
|
131
232
|
```
|
|
132
233
|
|
|
133
234
|
---
|
|
@@ -159,16 +260,13 @@ const page2 = await db.model('User').page(2, 20).find(); // Page 2, Size 20
|
|
|
159
260
|
|
|
160
261
|
```javascript
|
|
161
262
|
await db.model('User')
|
|
162
|
-
// Object style (AND logic)
|
|
163
263
|
.where({
|
|
164
264
|
age: 18,
|
|
165
265
|
status: 'active'
|
|
166
266
|
})
|
|
167
|
-
// Operator style
|
|
168
267
|
.where('create_time', '>', 1600000000)
|
|
169
|
-
// SQL Fragment style (Powerful!)
|
|
170
268
|
.where('id IS NOT NULL')
|
|
171
|
-
.find()
|
|
269
|
+
.find()
|
|
172
270
|
```
|
|
173
271
|
|
|
174
272
|
### Complex Where with Template Literals
|
|
@@ -221,14 +319,14 @@ const stats = await db.model('User')
|
|
|
221
319
|
const newUser = await db.model('User').insert({
|
|
222
320
|
username: 'neo',
|
|
223
321
|
email: 'neo@matrix.com'
|
|
224
|
-
})
|
|
322
|
+
})
|
|
225
323
|
// ID and Timestamps are automatically generated if configured in Schema
|
|
226
324
|
|
|
227
325
|
// Insert multiple (Batch)
|
|
228
326
|
await db.model('User').insert([
|
|
229
327
|
{ username: 'a' },
|
|
230
328
|
{ username: 'b' }
|
|
231
|
-
])
|
|
329
|
+
])
|
|
232
330
|
```
|
|
233
331
|
|
|
234
332
|
### Update
|
|
@@ -305,16 +403,15 @@ const result = await db.transaction(async (tx) => {
|
|
|
305
403
|
const user = await tx.model('User').insert({ username: 'alice' });
|
|
306
404
|
|
|
307
405
|
// 2. Read operation within transaction
|
|
308
|
-
const count = await tx.model('User').count()
|
|
406
|
+
const count = await tx.model('User').count()
|
|
309
407
|
|
|
310
408
|
// 3. Throwing an error will automatically ROLLBACK
|
|
311
409
|
if (count > 100) {
|
|
312
|
-
throw new Error('Limit reached')
|
|
410
|
+
throw new Error('Limit reached')
|
|
313
411
|
}
|
|
314
412
|
|
|
315
|
-
return user
|
|
316
|
-
})
|
|
317
|
-
// Automatically COMMITTED here if no error
|
|
413
|
+
return user
|
|
414
|
+
})
|
|
318
415
|
```
|
|
319
416
|
|
|
320
417
|
### Using Raw Postgres Transaction
|
|
@@ -323,7 +420,7 @@ const result = await db.transaction(async (tx) => {
|
|
|
323
420
|
await db.sql.begin(async (sql) => {
|
|
324
421
|
// sql is the transaction connection
|
|
325
422
|
await sql`INSERT INTO users (name) VALUES ('bob')`;
|
|
326
|
-
})
|
|
423
|
+
})
|
|
327
424
|
```
|
|
328
425
|
|
|
329
426
|
---
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const process = require('node:process');
|
|
8
|
+
|
|
9
|
+
// JS 关键字列表,防止类名冲突
|
|
10
|
+
const JS_KEYWORDS = new Set([
|
|
11
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete',
|
|
12
|
+
'do', 'else', 'export', 'extends', 'false', 'finally', 'for', 'function', 'if', 'import',
|
|
13
|
+
'in', 'instanceof', 'new', 'null', 'return', 'super', 'switch', 'this', 'throw', 'true',
|
|
14
|
+
'try', 'typeof', 'var', 'void', 'while', 'with', 'yield', 'let', 'static', 'enum', 'await',
|
|
15
|
+
'implements', 'interface', 'package', 'private', 'protected', 'public', 'arguments', 'eval'
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
// 帮助信息
|
|
19
|
+
function showHelp() {
|
|
20
|
+
console.log(`
|
|
21
|
+
Usage: neopg-model [options] [name1] [name2] ...
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--dir=<path> 指定模型文件保存目录 (默认: ./model)
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
neopg-model user-log order_info
|
|
28
|
+
neopg-model --dir=./src/models Product
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 解析参数
|
|
33
|
+
function parseArgs() {
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
const config = {
|
|
36
|
+
dir: './model',
|
|
37
|
+
names: []
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (args.length === 0) {
|
|
41
|
+
showHelp();
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const arg of args) {
|
|
46
|
+
if (arg.startsWith('--dir=')) {
|
|
47
|
+
config.dir = arg.split('=')[1];
|
|
48
|
+
} else if (!arg.startsWith('-')) {
|
|
49
|
+
config.names.push(arg);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 验证名称合法性: 字母开头,只允许字母、数字、_、-
|
|
57
|
+
function isValidName(name) {
|
|
58
|
+
return /^[a-zA-Z][a-zA-Z0-9_-]*$/.test(name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 处理命名规则
|
|
62
|
+
function processName(inputName) {
|
|
63
|
+
// 1. 移除扩展名,但记录是否是 .mjs
|
|
64
|
+
let isMjs = inputName.endsWith('.mjs');
|
|
65
|
+
const cleanName = inputName.replace(/(\.js|\.mjs)$/, '');
|
|
66
|
+
|
|
67
|
+
// 2. 验证合法性
|
|
68
|
+
if (!isValidName(cleanName)) {
|
|
69
|
+
console.error(`\x1b[31m[Error]\x1b[0m 名称 "${inputName}" 不合法。必须以字母开头,只能包含字母、数字、下划线和连字符。`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 3. 生成 tableName: 全小写,- 替换为 _
|
|
74
|
+
const tableName = cleanName.toLowerCase().replace(/-/g, '_');
|
|
75
|
+
|
|
76
|
+
// 4. 生成 modelName:
|
|
77
|
+
// - 去掉 '-'
|
|
78
|
+
// - '-' 后面的字母大写 (驼峰)
|
|
79
|
+
// - '_' 保留,_ 后面的字母不做特殊处理
|
|
80
|
+
// - 首字母大写
|
|
81
|
+
const parts = cleanName.split('-');
|
|
82
|
+
const modelName = parts.map((p, index) => {
|
|
83
|
+
// 每一部分的首字母大写
|
|
84
|
+
return p.charAt(0).toUpperCase() + p.slice(1);
|
|
85
|
+
}).join(''); // 直接连接,去掉了 -
|
|
86
|
+
|
|
87
|
+
// 检查关键字
|
|
88
|
+
if (JS_KEYWORDS.has(modelName)) {
|
|
89
|
+
console.warn(`\x1b[33m[Warning]\x1b[0m 生成的类名 "${modelName}" 是 JavaScript 关键字,建议修改名称。`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
raw: cleanName,
|
|
94
|
+
isMjs,
|
|
95
|
+
tableName,
|
|
96
|
+
modelName
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 生成 CommonJS 模板
|
|
101
|
+
function generateCJS(info) {
|
|
102
|
+
return `'use strict'\n\nconst { dataTypes, ModelChain } = require('neopg')
|
|
103
|
+
|
|
104
|
+
class ${info.modelName} extends ModelChain {
|
|
105
|
+
static schema = {
|
|
106
|
+
tableName: '${info.tableName}',
|
|
107
|
+
modelName: '${info.modelName}',
|
|
108
|
+
primaryKey: 'id',
|
|
109
|
+
column: {
|
|
110
|
+
id: {
|
|
111
|
+
type: dataTypes.ID
|
|
112
|
+
},
|
|
113
|
+
name: {
|
|
114
|
+
type: dataTypes.STRING(30),
|
|
115
|
+
default: ''
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
index: [],
|
|
119
|
+
unique: []
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = ${info.modelName}
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 生成 ESM 模板
|
|
128
|
+
function generateESM(info) {
|
|
129
|
+
return `'use strict'\n\nimport { dataTypes, ModelChain } from 'neopg'
|
|
130
|
+
|
|
131
|
+
class ${info.modelName} extends ModelChain {
|
|
132
|
+
static schema = {
|
|
133
|
+
tableName: '${info.tableName}',
|
|
134
|
+
modelName: '${info.modelName}',
|
|
135
|
+
primaryKey: 'id',
|
|
136
|
+
column: {
|
|
137
|
+
id: {
|
|
138
|
+
type: dataTypes.ID
|
|
139
|
+
},
|
|
140
|
+
name: {
|
|
141
|
+
type: dataTypes.STRING(30),
|
|
142
|
+
default: ''
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
index: [],
|
|
146
|
+
unique: []
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default ${info.modelName}
|
|
151
|
+
`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 主逻辑
|
|
155
|
+
function main() {
|
|
156
|
+
const config = parseArgs();
|
|
157
|
+
|
|
158
|
+
// 1. 确保目录存在
|
|
159
|
+
const targetDir = path.resolve(process.cwd(), config.dir);
|
|
160
|
+
if (!fs.existsSync(targetDir)) {
|
|
161
|
+
try {
|
|
162
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
163
|
+
console.log(`\x1b[32m[Info]\x1b[0m 创建目录: ${config.dir}`);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(`\x1b[31m[Error]\x1b[0m 无法创建目录 ${targetDir}: ${err.message}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (config.names.length === 0) {
|
|
171
|
+
console.error('\x1b[31m[Error]\x1b[0m 未指定模型名称。');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const name of config.names) {
|
|
176
|
+
const info = processName(name);
|
|
177
|
+
if (!info) continue;
|
|
178
|
+
|
|
179
|
+
const ext = info.isMjs ? '.mjs' : '.js';
|
|
180
|
+
const fileName = info.raw + ext;
|
|
181
|
+
const filePath = path.join(targetDir, fileName);
|
|
182
|
+
|
|
183
|
+
// 检查冲突:
|
|
184
|
+
// 1. 检查完全同名的文件
|
|
185
|
+
if (fs.existsSync(filePath)) {
|
|
186
|
+
console.error(`\x1b[31m[Skip]\x1b[0m 文件已存在: ${fileName}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 2. 检查 ModelName 命名的文件 (可选,防止 user.js 和 User.js 在某些系统混淆)
|
|
191
|
+
const modelNamePath = path.join(targetDir, info.modelName + ext);
|
|
192
|
+
if (fs.existsSync(modelNamePath) && modelNamePath.toLowerCase() !== filePath.toLowerCase()) {
|
|
193
|
+
console.warn(`\x1b[33m[Warning]\x1b[0m 存在同类名文件: ${info.modelName}${ext},可能会导致混淆。`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const content = info.isMjs ? generateESM(info) : generateCJS(info);
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
200
|
+
console.log(`\x1b[32m[Success]\x1b[0m 已创建模型: ${path.join(config.dir, fileName)} (Table: ${info.tableName}, Class: ${info.modelName})`);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(`\x1b[31m[Error]\x1b[0m 写入文件失败 ${fileName}: ${err.message}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
main();
|
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)
|
|
@@ -33,7 +37,7 @@ class NeoPG {
|
|
|
33
37
|
|
|
34
38
|
// --- 注册 ---
|
|
35
39
|
|
|
36
|
-
add(input) {
|
|
40
|
+
add(input, is_reset=false) {
|
|
37
41
|
let ModelClass
|
|
38
42
|
|
|
39
43
|
if (typeof input === 'function') {
|
|
@@ -43,10 +47,16 @@ class NeoPG {
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const rawSchema = ModelClass.schema
|
|
50
|
+
|
|
46
51
|
if (!rawSchema) throw new Error(`[NeoPG] Missing static schema for ${ModelClass.name}`)
|
|
47
52
|
|
|
48
53
|
const def = new ModelDef(rawSchema)
|
|
49
54
|
|
|
55
|
+
//已经存在又不是更新,则报错
|
|
56
|
+
if (!is_reset && this.registry.has(def.modelName)) {
|
|
57
|
+
throw new Error(`[NeoPG] modelName conflict: ${def.modelName}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
this.registry.set(def.modelName, {
|
|
51
61
|
Class: ModelClass,
|
|
52
62
|
def: def
|
|
@@ -59,6 +69,10 @@ class NeoPG {
|
|
|
59
69
|
return this.add(model)
|
|
60
70
|
}
|
|
61
71
|
|
|
72
|
+
set(model) {
|
|
73
|
+
return this.add(model, true)
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
// --- 事务 ---
|
|
63
77
|
async transaction(callback) {
|
|
64
78
|
return await this.driver.begin(async (trxSql) => {
|
|
@@ -126,6 +140,64 @@ class NeoPG {
|
|
|
126
140
|
async close() {
|
|
127
141
|
await this.driver.end()
|
|
128
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
|
+
|
|
129
201
|
}
|
|
130
202
|
|
|
131
203
|
NeoPG.dataTypes = dataTypes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neopg",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "orm for postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neopg",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"bugs": {
|
|
19
19
|
"url": "https://github.com/master-genius/neopg/issues"
|
|
20
20
|
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"neopg-model": "./bin/neopg-model.js"
|
|
23
|
+
},
|
|
21
24
|
"repository": {
|
|
22
25
|
"type": "git",
|
|
23
26
|
"url": "git+https://github.com/master-genius/neopg.git"
|
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')
|