neopg 0.0.1 → 1.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.md ADDED
@@ -0,0 +1,415 @@
1
+ ![](./images/neopg.png)
2
+
3
+ # NeoPG
4
+
5
+ ### The Next Generation PostgreSQL ORM for Node.js
6
+
7
+ **NeoPG** is a high-performance, zero-dependency ORM built directly on top of [postgres.js](https://github.com/porsager/postgres) — the fastest PostgreSQL client for Node.js.
8
+
9
+ It bridges the gap between the developer experience (DX) of a chainable Query Builder and the raw performance of native SQL Template Literals.
10
+
11
+ ### [🪭 中文文档 ☯️](./README.cn.md)
12
+
13
+ ## 🚀 Key Features
14
+
15
+ * **Powered by [postgres.js](https://github.com/porsager/postgres)**: Inherits the incredible speed and stability of the fastest PG client.
16
+ * **Zero Dependencies**: The core driver is vendored and optimized internally. No heavy dependency tree.
17
+ * **Hybrid API**: Enjoy the ease of **Chainable/Fluent APIs** (like `.where().select()`) combined with the power of **Tagged Template Literals**.
18
+ * **Performance First**: Zero string concatenation logic. All queries are compiled into efficient fragments and executed natively.
19
+ * **Auto Schema Sync**: define your models in code, and NeoPG syncs the table structure, indices, and foreign keys automatically.
20
+ * **Type Smart**: Automatic type casting for aggregations (`sum`, `avg` returns numbers, not strings) and JSON handling.
21
+
22
+ ---
23
+
24
+ ## 📦 Installation
25
+
26
+ ```bash
27
+ npm install neopg
28
+ ```
29
+
30
+ ---
31
+
32
+ ## 🔌 Initialization
33
+
34
+ ### Connect to Database
35
+
36
+ ```javascript
37
+ const NeoPG = require('neopg');
38
+
39
+ const config = {
40
+ host: 'localhost',
41
+ port: 5432,
42
+ database: 'my_db',
43
+ user: 'postgres',
44
+ password: 'password',
45
+ max: 10, // Connection pool size
46
+ idle_timeout: 30, // Idle connection timeout in seconds
47
+ debug: false, // Enable query logging
48
+ schema: 'public' // Default schema
49
+ };
50
+
51
+ const db = new NeoPG(config);
52
+ ```
53
+
54
+ ### Close Connection
55
+
56
+ ```javascript
57
+ await db.close()
58
+ ```
59
+
60
+ ---
61
+
62
+ ## 📝 Defining a Model
63
+
64
+ Create a model file (e.g., `models/User.js`). Your class should extend `NeoPG.ModelChain`.
65
+
66
+ ```javascript
67
+ const { ModelChain, dataTypes } = require('neopg')
68
+
69
+ class User extends ModelChain {
70
+ static schema = {
71
+ tableName: 'users',
72
+ modelName: 'User', // Optional, defaults to tableName
73
+ primaryKey: 'id',
74
+
75
+ // Auto-sync table structure based on this definition
76
+ column: {
77
+ id: {
78
+ type: dataTypes.ID, // Auto-generates Snowflake-like ID
79
+ },
80
+ username: {
81
+ type: dataTypes.STRING(100),
82
+ required: true
83
+ },
84
+ email: {
85
+ type: dataTypes.STRING(255),
86
+ required: true
87
+ },
88
+ age: {
89
+ type: dataTypes.INT,
90
+ default: 18
91
+ },
92
+ meta: {
93
+ type: dataTypes.JSONB
94
+ },
95
+ created_at: {
96
+ type: dataTypes.BIGINT,
97
+ timestamp: 'insert' // Auto-fill on insert
98
+ },
99
+ updated_at: {
100
+ type: dataTypes.BIGINT,
101
+ timestamp: 'update' // Auto-fill on insert & update
102
+ }
103
+ },
104
+
105
+ // Indexes
106
+ index: ['email', 'age'],
107
+ unique: ['username']
108
+ }
109
+ }
110
+
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
162
+ ```
163
+
164
+ ---
165
+
166
+ ## ⚙️ Registration & Sync
167
+
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.
177
+
178
+ ```javascript
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
+ })
197
+
198
+ ```
199
+
200
+ ### Syncing Database
201
+
202
+ Sync the table structure to the database based on registered models.
203
+
204
+ ```javascript
205
+ // Sync Table Structure (DDL)
206
+ // options: { force: true } will drop columns not defined in schema
207
+ await db.sync({ force: false })
208
+
209
+ console.log('Database synced!')
210
+ ```
211
+
212
+ ---
213
+
214
+ ## 🔍 Querying
215
+
216
+ NeoPG provides a fluent, chainable API that feels natural to use.
217
+
218
+ ### Basic Find
219
+
220
+ ```javascript
221
+ // Get all users
222
+ const users = await db.model('User').find();
223
+
224
+ // Select specific columns
225
+ const users = await db.model('User')
226
+ .select('id, username')
227
+ .limit(10)
228
+ .find();
229
+
230
+ // Get a single record
231
+ const user = await db.model('User').where({ id: '123' }).get();
232
+
233
+ // Pagination
234
+ const page2 = await db.model('User').page(2, 20).find(); // Page 2, Size 20
235
+ ```
236
+
237
+ ### Chained Where
238
+
239
+ ```javascript
240
+ await db.model('User')
241
+ // Object style (AND logic)
242
+ .where({
243
+ age: 18,
244
+ status: 'active'
245
+ })
246
+ // Operator style
247
+ .where('create_time', '>', 1600000000)
248
+ // SQL Fragment style (Powerful!)
249
+ .where('id IS NOT NULL')
250
+ .find();
251
+ ```
252
+
253
+ ### Complex Where with Template Literals
254
+
255
+ This is where NeoPG shines. You can mix raw SQL fragments safely using the `sql` tag from the context.
256
+
257
+ ```javascript
258
+ // db.sql is the native postgres instance
259
+ const { sql } = db;
260
+
261
+ await db.model('User')
262
+ .where({ status: 'active' })
263
+ // Safe parameter injection via Template Literals
264
+ .where(sql`age > ${20} AND email LIKE ${'%@gmail.com'}`)
265
+ .find();
266
+ ```
267
+
268
+ ---
269
+
270
+ ## 📊 Aggregation
271
+
272
+ NeoPG handles type casting automatically (e.g., converting PostgreSQL `count` string results to Javascript Numbers).
273
+
274
+ ```javascript
275
+ // Count
276
+ const total = await db.model('User').where({ age: 18 }).count();
277
+
278
+ // Max / Min
279
+ const maxAge = await db.model('User').max('age');
280
+
281
+ // Sum / Avg (Returns Number, not String)
282
+ const totalScore = await db.model('User').sum('score');
283
+ const avgScore = await db.model('User').avg('score');
284
+
285
+ // Group By
286
+ const stats = await db.model('User')
287
+ .select('city, count(*) as num')
288
+ .group('city')
289
+ .find();
290
+ ```
291
+
292
+ ---
293
+
294
+ ## ✏️ Write Operations
295
+
296
+ ### Insert
297
+
298
+ ```javascript
299
+ // Insert one
300
+ const newUser = await db.model('User').insert({
301
+ username: 'neo',
302
+ email: 'neo@matrix.com'
303
+ });
304
+ // ID and Timestamps are automatically generated if configured in Schema
305
+
306
+ // Insert multiple (Batch)
307
+ await db.model('User').insert([
308
+ { username: 'a' },
309
+ { username: 'b' }
310
+ ]);
311
+ ```
312
+
313
+ ### Update
314
+
315
+ ```javascript
316
+ const updated = await db.model('User')
317
+ .where({ id: '123' })
318
+ .update({
319
+ age: 99,
320
+ meta: { role: 'admin' }
321
+ });
322
+ ```
323
+
324
+ ### Delete
325
+
326
+ ```javascript
327
+ await db.model('User')
328
+ .where('age', '<', 10)
329
+ .delete();
330
+ ```
331
+
332
+ ### Returning Data
333
+
334
+ By default, write operations might not return data depending on the driver optimization. You can enforce it:
335
+
336
+ ```javascript
337
+ const deletedUsers = await db.model('User')
338
+ .where('status', 'banned')
339
+ .returning('id, username') // or returning('*')
340
+ .delete();
341
+ ```
342
+
343
+ ---
344
+
345
+ ## ⚡ Raw SQL (Template Literals)
346
+
347
+ NeoPG exposes the full power of `postgres.js`. You don't need the ModelChain for everything.
348
+
349
+ > 📚 **Reference**: Full documentation for the SQL tag can be found at the [postgres.js GitHub page](https://github.com/porsager/postgres).
350
+
351
+ ```javascript
352
+ // Access the native driver
353
+ const sql = db.sql;
354
+
355
+ // Execute raw SQL safely
356
+ const users = await sql`
357
+ SELECT * FROM users
358
+ WHERE age > ${20}
359
+ `;
360
+
361
+ // Dynamic tables/columns using helper
362
+ const table = 'users';
363
+ const column = 'age';
364
+ const result = await sql`
365
+ SELECT ${sql(column)}
366
+ FROM ${sql(table)}
367
+ `;
368
+ ```
369
+
370
+ ---
371
+
372
+ ## 🤝 Transactions
373
+
374
+ NeoPG provides a unified transaction API. It supports nested transactions (Savepoints) automatically.
375
+
376
+ ### Using NeoPG Context (Recommended)
377
+
378
+ ```javascript
379
+ // Start a transaction scope
380
+ const result = await db.transaction(async (tx) => {
381
+ // 'tx' is a TransactionScope that mimics 'db'
382
+
383
+ // 1. Write operation
384
+ const user = await tx.model('User').insert({ username: 'alice' });
385
+
386
+ // 2. Read operation within transaction
387
+ const count = await tx.model('User').count();
388
+
389
+ // 3. Throwing an error will automatically ROLLBACK
390
+ if (count > 100) {
391
+ throw new Error('Limit reached');
392
+ }
393
+
394
+ return user;
395
+ });
396
+ // Automatically COMMITTED here if no error
397
+ ```
398
+
399
+ ### Using Raw Postgres Transaction
400
+
401
+ ```javascript
402
+ await db.sql.begin(async (sql) => {
403
+ // sql is the transaction connection
404
+ await sql`INSERT INTO users (name) VALUES ('bob')`;
405
+ });
406
+ ```
407
+
408
+ ---
409
+
410
+ ## License
411
+
412
+ ISC
413
+
414
+
415
+ ![](./images/neopg-programming.jpeg)
@@ -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();
Binary file
Binary file