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/lib/ModelChain.js CHANGED
@@ -4,6 +4,18 @@ const makeId = require('./makeId.js')
4
4
  const makeTimestamp = require('./makeTimestamp.js')
5
5
  const TransactionScope = require('./TransactionScope.js')
6
6
 
7
+ // [优化 1] 提取常量定义到类外部,提升性能
8
+ const INT_TYPES = new Set([
9
+ 'int', 'integer', 'smallint', 'bigint',
10
+ 'serial', 'bigserial', 'smallserial',
11
+ 'int2', 'int4', 'int8'
12
+ ])
13
+
14
+ const FLOAT_TYPES = new Set([
15
+ 'float', 'double', 'numeric', 'decimal', 'real',
16
+ 'money', 'double precision', 'float4', 'float8'
17
+ ])
18
+
7
19
  /**
8
20
  * ModelChain - 链式查询构建器
9
21
  * 负责运行时的查询构建、SQL 拼装和结果处理。
@@ -30,6 +42,9 @@ class ModelChain {
30
42
  this._columns = null
31
43
  this._group = null
32
44
  this._lock = null
45
+ this._returning = null
46
+ this._joins = []
47
+ this._group = []
33
48
 
34
49
  // 内部状态标记
35
50
  this._isRaw = !!def.isRaw
@@ -163,6 +178,37 @@ class ModelChain {
163
178
  return this
164
179
  }
165
180
 
181
+ returning(cols) {
182
+ if (!cols) return this
183
+
184
+ if (typeof cols === 'string') {
185
+ // 支持 'id, name' 写法
186
+ this._returning = cols.split(',').map(s => s.trim()).filter(s => s)
187
+ } else if (Array.isArray(cols)) {
188
+ this._returning = cols
189
+ }
190
+
191
+ return this
192
+ }
193
+
194
+ // --- 构建 RETURNING 片段 ---
195
+ _buildReturning() {
196
+ // 如果没有设置 returning,默认不返回数据 (节省性能)
197
+ // 注意:这意味着默认 insert/update 返回的是 Result 对象(包含 count),而不是行数据
198
+ if (!this._returning || this._returning.length === 0) {
199
+ return this.sql``
200
+ }
201
+
202
+ // 特殊处理 '*': 用户显式要求 returning('*')
203
+ // 如果直接用 this.sql(['*']) 会被转义为 "*",导致错误
204
+ if (this._returning.length === 1 && this._returning[0] === '*') {
205
+ return this.sql`RETURNING *`
206
+ }
207
+
208
+ // 普通字段:利用 postgres.js 自动转义标识符
209
+ return this.sql`RETURNING ${this.sql(this._returning)}`
210
+ }
211
+
166
212
  // --- 辅助:构建 Where 片段 (修复 Bug 的核心) ---
167
213
  _buildWhere() {
168
214
  const len = this._conditions.length
@@ -208,18 +254,15 @@ class ModelChain {
208
254
  // 修复:使用新方法构建
209
255
  const w = this._buildWhere()
210
256
  const o = this._buildOrder()
257
+ const j = this._buildJoins()
258
+ const g = this._buildGroup()
211
259
 
212
260
  const l = this._limit ? this.sql`LIMIT ${this._limit}` : this.sql``
213
261
  const off = this._offset ? this.sql`OFFSET ${this._offset}` : this.sql``
214
262
  const lck = this._lock || this.sql``
215
263
  const ft = this.sql`${this.sql(this.schema)}.${t}`
216
264
 
217
- return await this.sql`SELECT ${c} FROM ${ft} ${w} ${o} ${l} ${off} ${lck}`
218
- }
219
-
220
- // Thenable 接口
221
- then(onFulfilled, onRejected) {
222
- return this.find().then(onFulfilled, onRejected)
265
+ return await this.sql`SELECT ${c} FROM ${ft} ${j} ${w} ${g} ${o} ${l} ${off} ${lck}`
223
266
  }
224
267
 
225
268
  async get() {
@@ -229,12 +272,16 @@ class ModelChain {
229
272
  }
230
273
 
231
274
  async count() {
232
- const t = this.sql(this.tableName);
233
- // 修复:使用新方法构建
275
+ const t = this.sql(this.tableName)
276
+
234
277
  const w = this._buildWhere()
235
278
  const ft = this.sql`${this.sql(this.schema)}.${t}`
279
+ const j = this._buildJoins()
236
280
 
237
- const r = await this.sql`SELECT count(*) as total FROM ${ft} ${w}`
281
+ const r = await this.sql`SELECT count(*) as total FROM ${ft} ${j} ${w}`
282
+
283
+ if (r.length === 0) return 0
284
+
238
285
  return parseInt(r[0].total)
239
286
  }
240
287
 
@@ -248,9 +295,24 @@ class ModelChain {
248
295
  }
249
296
 
250
297
  const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
251
- const result = await this.sql`INSERT INTO ${fullTable} ${this.sql(inputs)} RETURNING *`
252
298
 
253
- if (!isArray && result.length > 0) return result[0]
299
+ // [修改] 动态构建 returning
300
+ const retFragment = this._buildReturning()
301
+
302
+ const result = await this.sql`INSERT INTO ${fullTable} ${this.sql(inputs)} ${retFragment}`
303
+
304
+ // 如果有 returning 数据,result 是数组(包含行);否则 result 是 Result 对象(包含 count)
305
+ // 逻辑保持兼容:如果用户请求了数据,且是单条插入,返回对象;否则返回数组
306
+ if (this._returning && this._returning.length > 0) {
307
+ if (!isArray && result.length === 1) {
308
+ return result[0]
309
+ }
310
+
311
+ return result
312
+ }
313
+
314
+ // 如果没有 returning,返回 postgres 原生结果 (包含 count 等信息)
315
+ // 测试发现如果没有returning则返回的是空数组
254
316
  return result
255
317
  }
256
318
 
@@ -264,7 +326,18 @@ class ModelChain {
264
326
  // 修复:使用新方法构建
265
327
  const whereFragment = this._buildWhere()
266
328
 
267
- return await this.sql`UPDATE ${fullTable} SET ${this.sql(data)} ${whereFragment} RETURNING *`
329
+ // [修改] 动态构建 returning
330
+ const retFragment = this._buildReturning()
331
+
332
+ const result = await this.sql`UPDATE ${fullTable} SET ${this.sql(data)} ${whereFragment} ${retFragment}`
333
+
334
+ if (this._returning && this._returning.length > 0) {
335
+ if (result.length === 1) return result[0]
336
+
337
+ return result
338
+ }
339
+
340
+ return result
268
341
  }
269
342
 
270
343
  async delete() {
@@ -273,14 +346,249 @@ class ModelChain {
273
346
  // 修复:使用新方法构建
274
347
  const whereFragment = this._buildWhere()
275
348
 
276
- return await this.sql`DELETE FROM ${fullTable} ${whereFragment} RETURNING *`
349
+ const retFragment = this._buildReturning()
350
+
351
+ return await this.sql`DELETE FROM ${fullTable} ${whereFragment} ${retFragment}`
277
352
  }
278
353
 
279
354
  async transaction(callback) {
280
- return await this.sql.begin(async (trxSql) => {
355
+ return this.ctx.transaction(callback)
356
+ /* return await this.sql.begin(async (trxSql) => {
281
357
  const scope = new TransactionScope(this.ctx, trxSql)
282
358
  return await callback(scope)
283
- })
359
+ }) */
360
+ }
361
+
362
+ begin(callback) {
363
+ return this.ctx.transaction(callback)
364
+ }
365
+
366
+ /**
367
+ * 内部通用 Join 添加器
368
+ * @param {string} type - 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN'
369
+ * @param {string|Object} table - 表名 或 sql`fragment`
370
+ * @param {string|Object} on - 条件字符串 或 sql`fragment`
371
+ */
372
+ _addJoin(type, table, on) {
373
+ let tableFragment
374
+ let onFragment
375
+
376
+ // 1. 处理 Table
377
+ if (table.constructor && table.constructor.name === 'Query') {
378
+ tableFragment = table
379
+ } else {
380
+ // 默认作为当前 Schema 下的表名处理 "public"."table"
381
+ // 如果需要跨 Schema (e.g. "other.table"),请用户传入 sql`other.table`
382
+ tableFragment = this.sql(table)
383
+ }
384
+
385
+ // 2. 处理 ON 条件
386
+ if (on.constructor && on.constructor.name === 'Query') {
387
+ onFragment = on;
388
+ } else {
389
+ // 字符串情况,视为 Raw SQL (e.g. "u.id = p.uid")
390
+ // 因为 ON 条件通常包含操作符,无法简单参数化,必须 unsafe
391
+ onFragment = this.sql.unsafe(on);
392
+ }
393
+
394
+ // 3. 构建单个 Join 片段
395
+ // 格式: TYPE + table + ON + condition
396
+ const joinFragment = this.sql`${this.sql.unsafe(type)} ${tableFragment} ON ${onFragment}`
397
+
398
+ this._joins.push(joinFragment)
399
+ return this
400
+ }
401
+
402
+ join(table, on) {
403
+ return this._addJoin('INNER JOIN', table, on)
404
+ }
405
+
406
+ innerJoin(table, on) {
407
+ return this._addJoin('INNER JOIN', table, on)
408
+ }
409
+
410
+ leftJoin(table, on) {
411
+ return this._addJoin('LEFT JOIN', table, on)
412
+ }
413
+
414
+ rightJoin(table, on) {
415
+ return this._addJoin('RIGHT JOIN', table, on)
416
+ }
417
+
418
+ fullJoin(table, on) {
419
+ return this._addJoin('FULL OUTER JOIN', table, on)
420
+ }
421
+
422
+ _buildJoins() {
423
+ const len = this._joins.length
424
+ if (len === 0) return this.sql``
425
+
426
+ // 只有一个 Join,直接返回
427
+ if (len === 1) {
428
+ return this._joins[0]
429
+ }
430
+
431
+ // 多个 Join,必须用空格连接,不能用逗号
432
+ // 采用“平铺数组”高性能方案
433
+ const parts = new Array(len * 2 - 1)
434
+ const SPACE = this.sql.unsafe(' ')
435
+
436
+ for (let i = 0; i < len; i++) {
437
+ parts[i * 2] = this._joins[i]
438
+ if (i < len - 1) {
439
+ parts[i * 2 + 1] = SPACE
440
+ }
441
+ }
442
+
443
+ return this.sql`${parts}`
444
+ }
445
+
446
+ /**
447
+ * 添加 Group By 条件
448
+ * .group('category_id')
449
+ * .group('category_id, type')
450
+ * .group(['id', 'name'])
451
+ */
452
+ group(arg) {
453
+ if (!arg) return this
454
+
455
+ // 1. Fragment
456
+ if (arg.constructor && arg.constructor.name === 'Query') {
457
+ this._group.push(arg)
458
+ return this
459
+ }
460
+
461
+ // 2. Array
462
+ if (Array.isArray(arg)) {
463
+ arg.forEach(f => this.group(f))
464
+ return this
465
+ }
466
+
467
+ // 3. String
468
+ if (typeof arg === 'string') {
469
+ if (arg.includes(',')) {
470
+ // 'id, name' -> 拆分
471
+ arg.split(',').map(s => s.trim()).filter(s=>s).forEach(s => {
472
+ this._group.push(this.sql(s))
473
+ })
474
+ } else {
475
+ // 单个字段
476
+ this._group.push(this.sql(arg))
477
+ }
478
+ }
479
+
480
+ return this
481
+ }
482
+
483
+ // 构建 Group 片段
484
+ _buildGroup() {
485
+ if (this._group.length === 0) return this.sql``
486
+
487
+ // postgres.js 模板数组默认用逗号连接,正好符合 GROUP BY 语法
488
+ return this.sql`GROUP BY ${this._group}`
489
+ }
490
+
491
+ // --- 聚合函数 ---
492
+
493
+ async min(field) {
494
+ return this._aggregate('MIN', field)
495
+ }
496
+
497
+ async max(field) {
498
+ return this._aggregate('MAX', field)
499
+ }
500
+
501
+ async sum(field) {
502
+ return this._aggregate('SUM', field)
503
+ }
504
+
505
+ async avg(field) {
506
+ return this._aggregate('AVG', field)
507
+ }
508
+
509
+ /**
510
+ * 通用聚合执行器
511
+ * @param {string} func - MIN, MAX, SUM, AVG
512
+ * @param {string} field - 列名
513
+ */
514
+ async _aggregate(func, field) {
515
+ if (!field) throw new Error(`[NeoPG] ${func} requires a field name.`)
516
+
517
+ const t = this.sql(this.tableName)
518
+ const w = this._buildWhere()
519
+ const j = this._buildJoins()
520
+ const ft = this.sql`${this.sql(this.schema)}.${t}`
521
+
522
+ // 处理字段名 (可能是 'age' 也可能是 'users.age')
523
+ let colFragment;
524
+ if (field.constructor && field.constructor.name === 'Query') {
525
+ colFragment = field
526
+ } else {
527
+ colFragment = this.sql(field)
528
+ }
529
+
530
+ // SELECT MIN(age) as val ...
531
+ const query = this.sql`
532
+ SELECT ${this.sql.unsafe(func)}(${colFragment}) as val
533
+ FROM ${ft} ${j} ${w}
534
+ `
535
+
536
+ const result = await query
537
+ const val = result.length > 0 ? result[0].val : null
538
+
539
+ if (val === null) return null;
540
+
541
+ // 智能类型转换
542
+ return this._convertAggregateValue(val, field, func)
543
+ }
544
+
545
+ /**
546
+ * 智能转换聚合结果类型
547
+ * Postgres 对于 SUM/AVG/COUNT 经常返回字符串 (BigInt/Numeric),我们需要转回 Number
548
+ */
549
+ _convertAggregateValue(val, field, func) {
550
+ // 1. AVG 始终是浮点数
551
+ if (func === 'AVG') {
552
+ return parseFloat(val)
553
+ }
554
+
555
+ // 如果是 Raw Fragment,无法推断类型,直接返回原值(通常是 String)
556
+ if (typeof field !== 'string') return val
557
+
558
+ // 2. 尝试从 ModelDef 获取列定义
559
+ // field 可能是 'age' 也可能是 'u.age' (别名暂不支持自动推断,这里只处理简单列名)
560
+ const colDef = this.def && this.def.columns ? this.def.columns[field] : null
561
+
562
+ // 如果不知道列定义,尝试尽力猜测
563
+ if (!colDef) {
564
+ // 如果 val 是字符串且长得像数字
565
+ if (typeof val === 'string' && !isNaN(val)) {
566
+ // SUM 默认为数字
567
+ if (func === 'SUM') return parseFloat(val)
568
+ }
569
+
570
+ return val
571
+ }
572
+
573
+ // 5. [优化] 精确类型匹配
574
+ // 处理 'numeric(10,2)' -> 'numeric'
575
+ // 处理 'integer' -> 'integer'
576
+ const rawType = colDef.type.toLowerCase()
577
+ const parenIndex = rawType.indexOf('(')
578
+ const baseType = parenIndex > 0 ? rawType.substring(0, parenIndex).trim() : rawType
579
+
580
+ // 整数匹配
581
+ if (INT_TYPES.has(baseType)) {
582
+ return parseInt(val, 10)
583
+ }
584
+
585
+ // 浮点数匹配
586
+ if (FLOAT_TYPES.has(baseType)) {
587
+ return parseFloat(val)
588
+ }
589
+
590
+ // 其他 (Date, String, Boolean) 原样返回
591
+ return val
284
592
  }
285
593
 
286
594
  // --- 内部辅助方法 ---
package/lib/NeoPG.js CHANGED
@@ -33,8 +33,9 @@ class NeoPG {
33
33
 
34
34
  // --- 注册 ---
35
35
 
36
- add(input) {
37
- let ModelClass;
36
+ add(input, is_reset=false) {
37
+ let ModelClass
38
+
38
39
  if (typeof input === 'function') {
39
40
  ModelClass = input
40
41
  } else {
@@ -42,10 +43,16 @@ class NeoPG {
42
43
  }
43
44
 
44
45
  const rawSchema = ModelClass.schema
46
+
45
47
  if (!rawSchema) throw new Error(`[NeoPG] Missing static schema for ${ModelClass.name}`)
46
48
 
47
49
  const def = new ModelDef(rawSchema)
48
50
 
51
+ //已经存在又不是更新,则报错
52
+ if (!is_reset && this.registry.has(def.modelName)) {
53
+ throw new Error(`[NeoPG] modelName conflict: ${def.modelName}`)
54
+ }
55
+
49
56
  this.registry.set(def.modelName, {
50
57
  Class: ModelClass,
51
58
  def: def
@@ -54,6 +61,14 @@ class NeoPG {
54
61
  return this
55
62
  }
56
63
 
64
+ define(model) {
65
+ return this.add(model)
66
+ }
67
+
68
+ set(model) {
69
+ return this.add(model, true)
70
+ }
71
+
57
72
  // --- 事务 ---
58
73
  async transaction(callback) {
59
74
  return await this.driver.begin(async (trxSql) => {
@@ -62,6 +77,10 @@ class NeoPG {
62
77
  })
63
78
  }
64
79
 
80
+ begin(callback) {
81
+ return this.transaction(callback)
82
+ }
83
+
65
84
  // --- 同步 ---
66
85
  async sync(options = {}) {
67
86
  if (!options || typeof options !== 'object') {
@@ -75,6 +94,45 @@ class NeoPG {
75
94
  }
76
95
  }
77
96
 
97
+ /**
98
+ * 监听 Postgres 消息通道
99
+ * @param {string} channel - 通道名称
100
+ * @param {Function} callback - (payload) => {}
101
+ * @returns {Object} 包含 unlisten 方法的对象
102
+ */
103
+ async listen(channel, callback) {
104
+ // postgres.js 的 listen 返回一个 Promise<void>
105
+ // 但它内部会维持连接。我们需要提供一种方式来取消监听。
106
+ // postgres.js v3 使用 sql.listen(channel, cb) 并返回一个 state 对象用于 close
107
+ const listener = await this.sql.listen(channel, (payload) => {
108
+ // 可以在这里统一处理 JSON 解析等
109
+ try {
110
+ const data = JSON.parse(payload)
111
+ callback(data)
112
+ } catch (e) {
113
+ // 无法解析则返回原始字符串
114
+ callback(payload)
115
+ }
116
+ })
117
+
118
+ return {
119
+ // 返回一个句柄用于取消监听
120
+ close: () => listener.unlisten()
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 发送通知
126
+ * @param {string} channel
127
+ * @param {string|Object} payload
128
+ */
129
+ async notify(channel, payload) {
130
+ const data = typeof payload === 'object' ? JSON.stringify(payload) : payload
131
+
132
+ // 使用 sql.notify 是最高效的
133
+ await this.sql.notify(channel, data)
134
+ }
135
+
78
136
  async close() {
79
137
  await this.driver.end()
80
138
  }
package/lib/SchemaSync.js CHANGED
@@ -47,6 +47,18 @@ class SchemaSync {
47
47
  const tableName = def.tableName;
48
48
  const curTableName = `${schema}.${tableName}`;
49
49
 
50
+ // [递归锁初始化] 记录本次同步过程中已经处理过的 Model,防止死循环
51
+ if (!options.syncedModels) {
52
+ options.syncedModels = new Set();
53
+ }
54
+
55
+ // 如果该表在本次递归链中已经同步过,则跳过
56
+ if (options.syncedModels.has(def.modelName)) {
57
+ return;
58
+ }
59
+ // 标记当前表已开始同步
60
+ options.syncedModels.add(def.modelName);
61
+
50
62
  if (debug) console.log(`检测数据表 ${tableName} 的column...`);
51
63
 
52
64
  // 0. 检查列定义的合法性 (移植自 _checkFixColumn)
@@ -76,7 +88,7 @@ class SchemaSync {
76
88
  // 同步索引、约束、外键
77
89
  await this.syncIndex(sql, def, schema, curTableName, debug);
78
90
  await this.syncUnique(sql, def, schema, curTableName, debug);
79
- await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx);
91
+ await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options);
80
92
  return;
81
93
  }
82
94
 
@@ -84,7 +96,7 @@ class SchemaSync {
84
96
  // 获取现有列信息
85
97
  const cols = await sql`
86
98
  SELECT column_name, data_type, column_default, character_maximum_length,
87
- numeric_precision, numeric_scale, is_nullable
99
+ numeric_precision, numeric_scale, is_nullable, is_generated
88
100
  FROM information_schema.columns
89
101
  WHERE table_name = ${tableName}
90
102
  AND table_schema = ${schema}
@@ -103,7 +115,7 @@ class SchemaSync {
103
115
  await this.syncIndex(sql, def, schema, curTableName, debug);
104
116
  await this.syncUnique(sql, def, schema, curTableName, debug);
105
117
  await this.autoRemoveIndex(sql, def, schema, tableName, debug);
106
- await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx);
118
+ await this.syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options);
107
119
 
108
120
  if (debug) console.log(` - 表结构同步完成 (${tableName}) - `);
109
121
  }
@@ -263,12 +275,31 @@ class SchemaSync {
263
275
 
264
276
  // 7. Drop Not Exist (Force Mode)
265
277
  if (dropNotExistCol) {
266
- for (let k in inf) {
267
- if (!def.columns[k] && !renameTable[k]) {
268
- await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(k)}`);
278
+ for (let dbColName in inf) {
279
+ // 如果代码中没定义,且不是刚改名的
280
+ if (!def.columns[dbColName] && !renameTable[dbColName]) {
281
+
282
+ const dbCol = inf[dbColName];
283
+
284
+ // [核心逻辑] 如果是数据库层面的虚拟列 (is_generated == 'ALWAYS'),豁免,不删除
285
+ if (dbCol.is_generated === 'ALWAYS') {
286
+ if (debug) console.log(`[NeoPG] Ignoring DB-only generated column: ${dbColName}`);
287
+ continue;
288
+ }
289
+
290
+ // [额外建议] 如果是 identity 列 (自增列的一种新形式),通常也建议豁免,防止误删
291
+ // if (dbCol.is_identity === 'YES') continue;
292
+
293
+ if (debug) console.log(`Deleting unused column: ${dbColName}`);
294
+ await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(dbColName)}`);
269
295
  }
270
296
  }
271
297
  }
298
+ /* for (let k in inf) {
299
+ if (!def.columns[k] && !renameTable[k]) {
300
+ await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(k)}`);
301
+ }
302
+ } */
272
303
  }
273
304
 
274
305
  // --- 索引同步 ---
@@ -370,7 +401,7 @@ class SchemaSync {
370
401
  }
371
402
 
372
403
  // --- 外键同步 ---
373
- static async syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx) {
404
+ static async syncReferences(sql, def, schema, curTableName, schemaOid, debug, ctx, options) {
374
405
  // 1. 收集定义中的外键
375
406
  // 格式: { fkName: "xxx", createSql: "..." }
376
407
  let targetFKs = new Map();
@@ -382,27 +413,32 @@ class SchemaSync {
382
413
 
383
414
  // 解析 ref: "ModelName:colName"
384
415
  const [refModelName, refColName] = this._parseRef(col.ref, k);
416
+
417
+ // 查找被引用的 Model 定义
418
+ // 假设 ctx.registry.get 返回 { Class, def }
419
+ const targetModelItem = ctx.registry.get(refModelName);
385
420
 
386
- // 加载目标模型 (这里需要通过 ctx (NeoPG实例) 获取其他模型)
387
- // 假设 ctx.registry.get(refModelName) 能拿到
388
- // 或者如果是文件路径,尝试 require
389
-
390
- // 为了简化复刻,这里假设我们能拿到目标表名
391
- // 在原逻辑中是 require 文件,这里建议 NeoPG 注册机制解决
392
- // 这里做一个适配:
393
421
  let targetTableName = '';
394
- if (ctx.registry && ctx.registry.get(refModelName)) {
395
- targetTableName = ctx.registry.get(refModelName).def.tableName;
422
+
423
+ if (targetModelItem) {
424
+ targetTableName = targetModelItem.def.tableName;
425
+ // --- [关键] 递归同步 ---
426
+ // 在创建外键之前,确保目标表已经存在且结构最新
427
+ if (debug) console.log(`[Recursive Sync] Triggered by FK: ${def.modelName} -> ${refModelName}`);
428
+
429
+ await this.execute(sql, targetModelItem.def, ctx, options);
396
430
  } else {
397
- // 尝试作为表名直接使用 (Fallback)
398
- targetTableName = refModelName.toLowerCase();
431
+ // 目标模型未注册,可能是直接引用的表名 (Fallback)
432
+ if (debug) console.warn(`[NeoPG] Referenced model '${refModelName}' not found in registry. Using as table name.`);
433
+ targetTableName = refModelName.toLowerCase();
399
434
  }
400
435
 
401
- // 构建外键名
436
+ // 准备外键 SQL
402
437
  const fkName = `${def.tableName}_${k}_fkey`;
403
438
 
404
439
  // 构建 REFERENCES 子句
405
440
  let refSql = `REFERENCES ${schema}.${targetTableName} (${refColName})`;
441
+
406
442
  if (col.refActionUpdate) refSql += ` ON UPDATE ${col.refActionUpdate}`;
407
443
  else refSql += ` ON UPDATE CASCADE`; // 默认
408
444
 
@@ -497,14 +533,15 @@ class SchemaSync {
497
533
 
498
534
  static _parseRef(refstr, curColumn) {
499
535
  if (refstr.includes(':')) {
500
- const parts = refstr.split(':');
536
+ const parts = refstr.split(':')
501
537
  // 处理 Model:col 格式,取最后一部分做 col,前面做 Model
502
- const col = parts.pop();
503
- const model = parts.join(':');
504
- return [model, col];
538
+ const col = parts.pop()
539
+ const model = parts.join(':')
540
+ return [model, col]
505
541
  }
506
- return [refstr, curColumn];
542
+
543
+ return [refstr, curColumn]
507
544
  }
508
545
  }
509
546
 
510
- module.exports = SchemaSync;
547
+ module.exports = SchemaSync
@@ -24,6 +24,10 @@ class TransactionScope {
24
24
  return await callback(new TransactionScope(this.parent, sp))
25
25
  })
26
26
  }
27
+
28
+ begin(callback) {
29
+ return this.transaction(callback)
30
+ }
27
31
  }
28
32
 
29
33
  module.exports = TransactionScope
package/package.json CHANGED
@@ -1,16 +1,26 @@
1
1
  {
2
2
  "name": "neopg",
3
- "version": "0.0.1",
3
+ "version": "1.0.1",
4
4
  "description": "orm for postgres",
5
5
  "keywords": [
6
+ "neopg",
7
+ "NeoPG",
8
+ "pg",
9
+ "neo",
10
+ "pgorm",
11
+ "psqlorm",
6
12
  "postgres",
7
13
  "orm",
8
- "database"
14
+ "database",
15
+ "postgres.js"
9
16
  ],
10
17
  "homepage": "https://github.com/master-genius/neopg#readme",
11
18
  "bugs": {
12
19
  "url": "https://github.com/master-genius/neopg/issues"
13
20
  },
21
+ "bin": {
22
+ "neopg-model": "./bin/neopg-model.js"
23
+ },
14
24
  "repository": {
15
25
  "type": "git",
16
26
  "url": "git+https://github.com/master-genius/neopg.git"