neopg 0.0.0 → 0.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 +132 -167
- package/lib/ModelDef.js +4 -1
- package/lib/NeoPG.js +8 -2
- package/lib/SchemaSync.js +15 -11
- package/lib/TransactionScope.js +2 -2
- package/package.json +1 -1
- package/test/test-db.js +153 -20
package/lib/ModelChain.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
'use strict'
|
|
1
|
+
'use strict'
|
|
2
2
|
|
|
3
|
-
const makeId = require('./makeId.js')
|
|
4
|
-
const makeTimestamp = require('./makeTimestamp.js')
|
|
3
|
+
const makeId = require('./makeId.js')
|
|
4
|
+
const makeTimestamp = require('./makeTimestamp.js')
|
|
5
|
+
const TransactionScope = require('./TransactionScope.js')
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* ModelChain - 链式查询构建器
|
|
@@ -50,68 +51,54 @@ class ModelChain {
|
|
|
50
51
|
* 3. .where('age', '>', 18) -> age > 18 (兼容)
|
|
51
52
|
*/
|
|
52
53
|
where(arg1, arg2, arg3) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// 依据指令:直接检测构造函数名称是否为 'Query'
|
|
57
|
-
if (arg1.constructor && arg1.constructor.name === 'Query') {
|
|
54
|
+
if (!arg1) return this
|
|
55
|
+
// 1. Fragment 检测
|
|
56
|
+
if (arg1.constructor && arg1.constructor.name === 'Query') {
|
|
58
57
|
this._conditions.push(arg1)
|
|
59
58
|
return this
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// 2. 对象写法 .where({ id: 1, name: 'Neo' })
|
|
63
|
-
if (typeof arg1 === 'object' && !Array.isArray(arg1)) {
|
|
64
|
-
const keys = Object.keys(arg1)
|
|
65
|
-
if (keys.length === 0) return this
|
|
66
|
-
|
|
67
|
-
for (const key of keys) {
|
|
68
|
-
const val = arg1[key];
|
|
69
|
-
if (val === undefined) continue
|
|
70
|
-
|
|
71
|
-
if (val === null) {
|
|
72
|
-
this._conditions.push(this.sql`${this.sql(key)} IS NULL`)
|
|
73
|
-
} else if (Array.isArray(val)) {
|
|
74
|
-
this._conditions.push(this.sql`${this.sql(key)} IN ${this.sql(val)}`)
|
|
75
|
-
} else {
|
|
76
|
-
this._conditions.push(this.sql`${this.sql(key)} = ${val}`)
|
|
77
|
-
}
|
|
78
59
|
}
|
|
60
|
+
|
|
61
|
+
// 2. Object 写法
|
|
62
|
+
if (typeof arg1 === 'object' && !Array.isArray(arg1)) {
|
|
63
|
+
for (const k of Object.keys(arg1)) {
|
|
64
|
+
const v = arg1[k]
|
|
79
65
|
|
|
80
|
-
|
|
81
|
-
}
|
|
66
|
+
if (v === undefined) continue
|
|
82
67
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
return this
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Case B: .where('age', '>', 18)
|
|
92
|
-
if (arg3 !== undefined) {
|
|
93
|
-
// 注意:中间的操作符必须用 sql.unsafe,因为它不是变量
|
|
94
|
-
this._conditions.push(this.sql`${this.sql(arg1)} ${this.sql.unsafe(arg2)} ${arg3}`)
|
|
68
|
+
if (v === null) this._conditions.push(this.sql`${this.sql(k)} IS NULL`)
|
|
69
|
+
else if (Array.isArray(v)) this._conditions.push(this.sql`${this.sql(k)} IN ${this.sql(v)}`)
|
|
70
|
+
else this._conditions.push(this.sql`${this.sql(k)} = ${v}`)
|
|
71
|
+
}
|
|
72
|
+
|
|
95
73
|
return this
|
|
96
74
|
}
|
|
97
75
|
|
|
98
|
-
//
|
|
99
|
-
if (arg1
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
76
|
+
// 3. String 写法
|
|
77
|
+
if (typeof arg1 === 'string') {
|
|
78
|
+
// .where('age', '>', 18)
|
|
79
|
+
if (arg3 !== undefined) {
|
|
80
|
+
this._conditions.push(this.sql`${this.sql(arg1)} ${this.sql.unsafe(arg2)} ${arg3}`)
|
|
81
|
+
return this
|
|
82
|
+
}
|
|
83
|
+
// .where('age', 18) -> age = 18
|
|
84
|
+
if (arg2 !== undefined) {
|
|
85
|
+
this._conditions.push(this.sql`${this.sql(arg1)} = ${arg2}`)
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
// .where('id = ?', 123)
|
|
89
|
+
if (arg1.includes('?') && arg2 !== undefined) {
|
|
90
|
+
const p = arg1.split('?');
|
|
91
|
+
if(p.length===2) {
|
|
92
|
+
this._conditions.push(this.sql`${this.sql.unsafe(p[0])}${arg2}${this.sql.unsafe(p[1])}`)
|
|
93
|
+
return this
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// .where('1=1') -> Raw SQL
|
|
97
|
+
// 注意:这里必须用 unsafe,否则 '1=1' 会被当成字符串值处理
|
|
98
|
+
this._conditions.push(this.sql.unsafe(arg1))
|
|
107
99
|
}
|
|
108
|
-
|
|
109
|
-
// Case D: 纯字符串 (视为 Raw SQL)
|
|
110
|
-
// .where("status = 'active'")
|
|
111
|
-
this._conditions.push(this.sql.unsafe(arg1))
|
|
112
|
-
}
|
|
113
100
|
|
|
114
|
-
|
|
101
|
+
return this
|
|
115
102
|
}
|
|
116
103
|
|
|
117
104
|
whereIf(condition, arg1, arg2, arg3) {
|
|
@@ -131,43 +118,29 @@ class ModelChain {
|
|
|
131
118
|
return this
|
|
132
119
|
}
|
|
133
120
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
* .order(sql`create_time DESC`)
|
|
137
|
-
* .order('create_time', 'DESC')
|
|
138
|
-
* .order({ create_time: 'DESC' })
|
|
139
|
-
*/
|
|
140
|
-
order(arg1, arg2) {
|
|
141
|
-
if (!arg1) return this
|
|
121
|
+
orderby(a, b) {
|
|
122
|
+
if(!a) return this
|
|
142
123
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return this
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// 2. Object { id: 'DESC' }
|
|
150
|
-
if (typeof arg1 === 'object') {
|
|
151
|
-
for (const key in arg1) {
|
|
152
|
-
const dir = arg1[key].toUpperCase()
|
|
153
|
-
this._order.push(this.sql`${this.sql(key)} ${this.sql.unsafe(dir)}`)
|
|
124
|
+
if(a.constructor && a.constructor.name==='Query') {
|
|
125
|
+
this._order.push(a)
|
|
126
|
+
return this
|
|
154
127
|
}
|
|
155
128
|
|
|
156
|
-
|
|
157
|
-
|
|
129
|
+
if(typeof a==='object') {
|
|
130
|
+
for(const k in a) {
|
|
131
|
+
this._order.push(this.sql`${this.sql(k)} ${this.sql.unsafe(a[k].toUpperCase())}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
158
136
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (arg1.includes(' ')) {
|
|
164
|
-
this._order.push(this.sql.unsafe(arg1))
|
|
165
|
-
} else {
|
|
166
|
-
this._order.push(this.sql`${this.sql(arg1)} ${this.sql.unsafe(dir)}`)
|
|
137
|
+
if(typeof a==='string') {
|
|
138
|
+
const d = b ? b.toUpperCase() : 'ASC'
|
|
139
|
+
if(a.includes(' ')) this._order.push(this.sql.unsafe(a));
|
|
140
|
+
else this._order.push(this.sql`${this.sql(a)} ${this.sql.unsafe(d)}`);
|
|
167
141
|
}
|
|
168
|
-
}
|
|
169
142
|
|
|
170
|
-
|
|
143
|
+
return this
|
|
171
144
|
}
|
|
172
145
|
|
|
173
146
|
limit(count, offset = 0) {
|
|
@@ -190,44 +163,58 @@ class ModelChain {
|
|
|
190
163
|
return this
|
|
191
164
|
}
|
|
192
165
|
|
|
193
|
-
|
|
166
|
+
// --- 辅助:构建 Where 片段 (修复 Bug 的核心) ---
|
|
167
|
+
_buildWhere() {
|
|
168
|
+
const len = this._conditions.length
|
|
169
|
+
if (len === 0) return this.sql``
|
|
170
|
+
|
|
171
|
+
// 只有一个条件,直接返回,零开销
|
|
172
|
+
if (len === 1) {
|
|
173
|
+
return this.sql`WHERE ${this._conditions[0]}`
|
|
174
|
+
}
|
|
194
175
|
|
|
195
|
-
|
|
196
|
-
|
|
176
|
+
// 预分配数组:N个条件需要 N-1 个 'AND',总长 2N-1
|
|
177
|
+
// 使用 new Array 预分配内存,比 push 更快
|
|
178
|
+
const parts = new Array(len * 2 - 1)
|
|
179
|
+
const AND = this.sql.unsafe(' AND ')
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < len; i++) {
|
|
182
|
+
// 偶数位放条件
|
|
183
|
+
parts[i * 2] = this._conditions[i]
|
|
184
|
+
// 奇数位放 AND (除了最后一位)
|
|
185
|
+
if (i < len - 1) {
|
|
186
|
+
parts[i * 2 + 1] = AND
|
|
187
|
+
}
|
|
188
|
+
}
|
|
197
189
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
190
|
+
// postgres.js 会自动展开这个扁平数组,性能极高
|
|
191
|
+
return this.sql`WHERE ${parts}`
|
|
192
|
+
}
|
|
201
193
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
194
|
+
// --- 辅助:构建 Order 片段 (修复 Bug) ---
|
|
195
|
+
_buildOrder() {
|
|
196
|
+
if (this._order.length === 0) return this.sql``
|
|
197
|
+
// 数组直接传入模板,postgres.js 默认用逗号连接,这正是 ORDER BY 需要的
|
|
198
|
+
// 不能用 this.sql(this._order),那样会试图转义为标识符
|
|
199
|
+
return this.sql`ORDER BY ${this._order}`
|
|
200
|
+
}
|
|
205
201
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
? this.sql`LIMIT ${this._limit}`
|
|
212
|
-
: this.sql``
|
|
213
|
-
|
|
214
|
-
const offsetFragment = this._offset
|
|
215
|
-
? this.sql`OFFSET ${this._offset}`
|
|
216
|
-
: this.sql``
|
|
202
|
+
// --- 核心:执行方法 ---
|
|
203
|
+
|
|
204
|
+
async find() {
|
|
205
|
+
const t = this.sql(this.tableName)
|
|
206
|
+
const c = this._columns ? this.sql(this._columns) : this.sql`*`
|
|
217
207
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
${offsetFragment}
|
|
229
|
-
${lockFragment}
|
|
230
|
-
`
|
|
208
|
+
// 修复:使用新方法构建
|
|
209
|
+
const w = this._buildWhere()
|
|
210
|
+
const o = this._buildOrder()
|
|
211
|
+
|
|
212
|
+
const l = this._limit ? this.sql`LIMIT ${this._limit}` : this.sql``
|
|
213
|
+
const off = this._offset ? this.sql`OFFSET ${this._offset}` : this.sql``
|
|
214
|
+
const lck = this._lock || this.sql``
|
|
215
|
+
const ft = this.sql`${this.sql(this.schema)}.${t}`
|
|
216
|
+
|
|
217
|
+
return await this.sql`SELECT ${c} FROM ${ft} ${w} ${o} ${l} ${off} ${lck}`
|
|
231
218
|
}
|
|
232
219
|
|
|
233
220
|
// Thenable 接口
|
|
@@ -242,22 +229,15 @@ class ModelChain {
|
|
|
242
229
|
}
|
|
243
230
|
|
|
244
231
|
async count() {
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
232
|
+
const t = this.sql(this.tableName);
|
|
233
|
+
// 修复:使用新方法构建
|
|
234
|
+
const w = this._buildWhere()
|
|
235
|
+
const ft = this.sql`${this.sql(this.schema)}.${t}`
|
|
249
236
|
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
const result = await this.sql`
|
|
253
|
-
SELECT count(*) as total FROM ${fullTable} ${whereFragment}
|
|
254
|
-
`
|
|
255
|
-
|
|
256
|
-
return parseInt(result[0].total)
|
|
237
|
+
const r = await this.sql`SELECT count(*) as total FROM ${ft} ${w}`
|
|
238
|
+
return parseInt(r[0].total)
|
|
257
239
|
}
|
|
258
240
|
|
|
259
|
-
// --- 写入方法 ---
|
|
260
|
-
|
|
261
241
|
async insert(data) {
|
|
262
242
|
const isArray = Array.isArray(data)
|
|
263
243
|
const inputs = isArray ? data : [data]
|
|
@@ -268,54 +248,39 @@ class ModelChain {
|
|
|
268
248
|
}
|
|
269
249
|
|
|
270
250
|
const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
|
|
271
|
-
|
|
272
|
-
const result = await this.sql`
|
|
273
|
-
INSERT INTO ${fullTable} ${this.sql(inputs)}
|
|
274
|
-
RETURNING *
|
|
275
|
-
`
|
|
251
|
+
const result = await this.sql`INSERT INTO ${fullTable} ${this.sql(inputs)} RETURNING *`
|
|
276
252
|
|
|
277
253
|
if (!isArray && result.length > 0) return result[0]
|
|
278
|
-
|
|
279
254
|
return result
|
|
280
255
|
}
|
|
281
256
|
|
|
282
257
|
async update(data) {
|
|
283
258
|
if (!data || Object.keys(data).length === 0) throw new Error('[NeoPG] Update data cannot be empty')
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (this._conditions.length === 0) {
|
|
290
|
-
throw new Error('[NeoPG] UPDATE requires a WHERE condition')
|
|
291
|
-
}
|
|
292
|
-
|
|
259
|
+
if (this.def) { this._prepareDataForUpdate(data) }
|
|
260
|
+
|
|
261
|
+
if (this._conditions.length === 0) throw new Error('[NeoPG] UPDATE requires a WHERE condition')
|
|
262
|
+
|
|
293
263
|
const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return await this.sql`
|
|
298
|
-
UPDATE ${fullTable}
|
|
299
|
-
SET ${this.sql(data)}
|
|
300
|
-
${whereFragment}
|
|
301
|
-
RETURNING *
|
|
302
|
-
`
|
|
264
|
+
// 修复:使用新方法构建
|
|
265
|
+
const whereFragment = this._buildWhere()
|
|
266
|
+
|
|
267
|
+
return await this.sql`UPDATE ${fullTable} SET ${this.sql(data)} ${whereFragment} RETURNING *`
|
|
303
268
|
}
|
|
304
269
|
|
|
305
270
|
async delete() {
|
|
306
|
-
if (this._conditions.length === 0)
|
|
307
|
-
throw new Error('[NeoPG] DELETE requires a WHERE condition')
|
|
308
|
-
}
|
|
309
|
-
|
|
271
|
+
if (this._conditions.length === 0) throw new Error('[NeoPG] DELETE requires a WHERE condition')
|
|
310
272
|
const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
|
|
273
|
+
// 修复:使用新方法构建
|
|
274
|
+
const whereFragment = this._buildWhere()
|
|
275
|
+
|
|
276
|
+
return await this.sql`DELETE FROM ${fullTable} ${whereFragment} RETURNING *`
|
|
277
|
+
}
|
|
311
278
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
RETURNING *
|
|
318
|
-
`
|
|
279
|
+
async transaction(callback) {
|
|
280
|
+
return await this.sql.begin(async (trxSql) => {
|
|
281
|
+
const scope = new TransactionScope(this.ctx, trxSql)
|
|
282
|
+
return await callback(scope)
|
|
283
|
+
})
|
|
319
284
|
}
|
|
320
285
|
|
|
321
286
|
// --- 内部辅助方法 ---
|
package/lib/ModelDef.js
CHANGED
|
@@ -10,6 +10,9 @@ class ModelDef {
|
|
|
10
10
|
this.modelName = rawSchema.modelName || rawSchema.tableName
|
|
11
11
|
this.primaryKey = rawSchema.primaryKey || 'id'
|
|
12
12
|
this.columns = rawSchema.column || {}
|
|
13
|
+
this.index = rawSchema.index || []
|
|
14
|
+
this.unique = rawSchema.unique || []
|
|
15
|
+
this.removeIndex = rawSchema.removeIndex || []
|
|
13
16
|
|
|
14
17
|
this._parseColumns()
|
|
15
18
|
|
|
@@ -57,7 +60,7 @@ class ModelDef {
|
|
|
57
60
|
throw new Error(`[NeoPG] Column name '${k}' in table '${this.tableName}' is FORBIDDEN.`)
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
if (
|
|
63
|
+
if (!(/^[a-z][a-z0-9_]*$/i).test(k)) {
|
|
61
64
|
throw new Error(`[NeoPG] Column name '${k}' is invalid. Only alphanumeric and underscore allowed, must start with letter.`)
|
|
62
65
|
}
|
|
63
66
|
}
|
package/lib/NeoPG.js
CHANGED
|
@@ -20,7 +20,7 @@ class NeoPG {
|
|
|
20
20
|
|
|
21
21
|
table(tableName, schema = null) {
|
|
22
22
|
const target = schema || this.defaultSchema
|
|
23
|
-
return new this.ModelChain(this
|
|
23
|
+
return new this.ModelChain(this, {tableName, isRaw: true}, target)
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
model(name, schema = null) {
|
|
@@ -28,7 +28,7 @@ class NeoPG {
|
|
|
28
28
|
if (!item) throw new Error(`[NeoPG] Model '${name}' not found.`)
|
|
29
29
|
|
|
30
30
|
const target = schema || this.defaultSchema
|
|
31
|
-
return new item.Class(this
|
|
31
|
+
return new item.Class(this, item.def, target)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// --- 注册 ---
|
|
@@ -64,6 +64,12 @@ class NeoPG {
|
|
|
64
64
|
|
|
65
65
|
// --- 同步 ---
|
|
66
66
|
async sync(options = {}) {
|
|
67
|
+
if (!options || typeof options !== 'object') {
|
|
68
|
+
options = {}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!options.schema) options.schema = this.defaultSchema
|
|
72
|
+
|
|
67
73
|
for (const item of this.registry.values()) {
|
|
68
74
|
await SchemaSync.execute(this.driver, item.def, this, options)
|
|
69
75
|
}
|
package/lib/SchemaSync.js
CHANGED
|
@@ -32,14 +32,18 @@ const DefaultWithType = ['varchar', 'char', 'text', 'bytea', 'timestamp', 'times
|
|
|
32
32
|
|
|
33
33
|
class SchemaSync {
|
|
34
34
|
|
|
35
|
+
static async createSchema(sql, schema) {
|
|
36
|
+
return sql.unsafe(`create schema if not exists ${schema};`)
|
|
37
|
+
}
|
|
38
|
+
|
|
35
39
|
/**
|
|
36
40
|
* 入口方法
|
|
37
41
|
*/
|
|
38
42
|
static async execute(sql, def, ctx, options = {}) {
|
|
39
43
|
const debug = options.debug || false;
|
|
40
44
|
const force = options.force || false;
|
|
41
|
-
const dropNotExistCol = options.dropNotExistCol || false;
|
|
42
|
-
const schema = ctx.defaultSchema || 'public';
|
|
45
|
+
const dropNotExistCol = force || options.dropNotExistCol || false;
|
|
46
|
+
const schema = options.schema || ctx.defaultSchema || 'public';
|
|
43
47
|
const tableName = def.tableName;
|
|
44
48
|
const curTableName = `${schema}.${tableName}`;
|
|
45
49
|
|
|
@@ -130,7 +134,7 @@ class SchemaSync {
|
|
|
130
134
|
|
|
131
135
|
if (col.default !== undefined) {
|
|
132
136
|
if (col.default === null) line += ' default null';
|
|
133
|
-
else line += ` default
|
|
137
|
+
else line += ` default $_${qtag}_$${col.default}$_${qtag}_$`;
|
|
134
138
|
}
|
|
135
139
|
}
|
|
136
140
|
colSqls.push(line);
|
|
@@ -190,7 +194,7 @@ class SchemaSync {
|
|
|
190
194
|
|
|
191
195
|
if (col.default !== undefined) {
|
|
192
196
|
if (col.default === null) addSql += ' default null';
|
|
193
|
-
else addSql += ` default
|
|
197
|
+
else addSql += ` default $_${qtag}_$${col.default}$_${qtag}_$`;
|
|
194
198
|
}
|
|
195
199
|
|
|
196
200
|
if (debug) console.log(addSql);
|
|
@@ -215,7 +219,7 @@ class SchemaSync {
|
|
|
215
219
|
await sql.unsafe(`ALTER TABLE ${curTableName} DROP COLUMN ${this.fmtColName(k)}`);
|
|
216
220
|
let reAddSql = `ALTER TABLE ${curTableName} ADD COLUMN ${this.fmtColName(k)} ${col.type}`;
|
|
217
221
|
if (col.notNull !== false) reAddSql += ' not null';
|
|
218
|
-
if (col.default !== undefined) reAddSql += ` default
|
|
222
|
+
if (col.default !== undefined) reAddSql += ` default $_${qtag}_$${col.default}$_${qtag}_$`;
|
|
219
223
|
await sql.unsafe(reAddSql);
|
|
220
224
|
col.changed = true; // 标记变更,供外键处理使用
|
|
221
225
|
continue;
|
|
@@ -240,7 +244,7 @@ class SchemaSync {
|
|
|
240
244
|
// 简单比对逻辑 (注:PG存储的默认值格式可能不同,这里仅作简单触发)
|
|
241
245
|
// 实际生产中可能需要更复杂的解析,这里保留原逻辑结构
|
|
242
246
|
// 原逻辑用了 _realDefault 方法,这里我们简单处理,仅当需要时设置
|
|
243
|
-
let default_val_sql = col.default === null ? 'null' :
|
|
247
|
+
let default_val_sql = col.default === null ? 'null' : `$_${qtag}_$${col.default}$_${qtag}_$`;
|
|
244
248
|
// 这里为了简化,每次都重设默认值(开销很小),或者你需要实现 _realDefault
|
|
245
249
|
await sql.unsafe(`ALTER TABLE ${curTableName} ALTER COLUMN ${this.fmtColName(k)} SET DEFAULT ${default_val_sql}`);
|
|
246
250
|
}
|
|
@@ -271,12 +275,12 @@ class SchemaSync {
|
|
|
271
275
|
static async syncIndex(sql, def, schema, curTableName, debug) {
|
|
272
276
|
// 假设索引定义在 def.rawSchema.index (数组)
|
|
273
277
|
// ModelDef 需要暴露这个属性,或 def.indices
|
|
274
|
-
const indices = def.
|
|
278
|
+
const indices = def.index || [];
|
|
275
279
|
if (!Array.isArray(indices)) return;
|
|
276
280
|
|
|
277
281
|
for (const indname of indices) {
|
|
278
282
|
// 检查 removeIndex 配置
|
|
279
|
-
const removeIndex = def.
|
|
283
|
+
const removeIndex = def.removeIndex || [];
|
|
280
284
|
if (removeIndex.includes(indname)) continue;
|
|
281
285
|
|
|
282
286
|
// 检查列是否存在
|
|
@@ -310,7 +314,7 @@ class SchemaSync {
|
|
|
310
314
|
}
|
|
311
315
|
|
|
312
316
|
static async syncUnique(sql, def, schema, curTableName, debug) {
|
|
313
|
-
const uniques = def.
|
|
317
|
+
const uniques = def.unique || [];
|
|
314
318
|
if (!Array.isArray(uniques)) return;
|
|
315
319
|
|
|
316
320
|
for (const indname of uniques) {
|
|
@@ -347,8 +351,8 @@ class SchemaSync {
|
|
|
347
351
|
const currentIdxNames = allIdx.map(i => i.indexname);
|
|
348
352
|
|
|
349
353
|
// 2. 计算应该保留的索引名
|
|
350
|
-
const indices = def.
|
|
351
|
-
const uniques = def.
|
|
354
|
+
const indices = def.index || [];
|
|
355
|
+
const uniques = def.unique || [];
|
|
352
356
|
|
|
353
357
|
const keepSet = new Set();
|
|
354
358
|
const makeName = (n) => `${tableName}_${n.split(',').map(x=>x.trim()).filter(x=>x).join('_')}_idx`;
|
package/lib/TransactionScope.js
CHANGED
|
@@ -9,14 +9,14 @@ class TransactionScope {
|
|
|
9
9
|
|
|
10
10
|
table(tableName, schema = null) {
|
|
11
11
|
const target = schema || this.parent.defaultSchema
|
|
12
|
-
return new this.parent.ModelChain(this
|
|
12
|
+
return new this.parent.ModelChain(this, {tableName, isRaw: true}, target)
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
model(name, schema = null) {
|
|
16
16
|
const item = this.parent.registry.get(name)
|
|
17
17
|
if (!item) throw new Error(`[NeoPG] Model '${name}' not found.`)
|
|
18
18
|
const target = schema || this.parent.defaultSchema
|
|
19
|
-
return new item.Class(this
|
|
19
|
+
return new item.Class(this, item.def, target)
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async transaction(callback) {
|
package/package.json
CHANGED
package/test/test-db.js
CHANGED
|
@@ -1,44 +1,177 @@
|
|
|
1
1
|
// index.js
|
|
2
2
|
const NeoPG = require('../lib/NeoPG.js')
|
|
3
|
-
const ModelChain = NeoPG
|
|
3
|
+
const {dataTypes, ModelChain} = NeoPG
|
|
4
4
|
|
|
5
5
|
// 1. 初始化
|
|
6
6
|
const db = new NeoPG({
|
|
7
7
|
host: '127.0.0.1',
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
user: 'wy',
|
|
9
|
+
database: 'wdata',
|
|
10
|
+
schema: 'test'
|
|
10
11
|
});
|
|
11
12
|
|
|
12
13
|
// 2. 定义模型
|
|
13
|
-
const
|
|
14
|
+
const User = {
|
|
14
15
|
modelName: 'User',
|
|
15
|
-
tableName: '
|
|
16
|
+
tableName: 'users',
|
|
17
|
+
primaryKey: 'id',
|
|
18
|
+
|
|
16
19
|
column: {
|
|
17
|
-
id: {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
id : {
|
|
21
|
+
type: dataTypes.UID
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @type {column}
|
|
26
|
+
*/
|
|
27
|
+
username: {
|
|
28
|
+
type: dataTypes.STRING(50)
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
mobile: {
|
|
32
|
+
type: dataTypes.STRING(16)
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
mobile_state: {
|
|
36
|
+
type: dataTypes.SMALLINT,
|
|
37
|
+
default: 0
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
//真实姓名
|
|
41
|
+
realname: {
|
|
42
|
+
type: dataTypes.STRING(50),
|
|
43
|
+
default: ''
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
sex: {
|
|
47
|
+
type: dataTypes.SMALLINT,
|
|
48
|
+
default: 0
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
//其他信息,以JSON格式存储
|
|
52
|
+
info: {
|
|
53
|
+
type: dataTypes.TEXT,
|
|
54
|
+
default: ''
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @type {column}
|
|
59
|
+
*/
|
|
60
|
+
passwd: {
|
|
61
|
+
type: dataTypes.STRING(240)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
//当验证失败,需要进行重复密码验证
|
|
65
|
+
repasswd: {
|
|
66
|
+
type: dataTypes.STRING(240)
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
//通过触摸手势或轨迹生成的密码
|
|
70
|
+
touchpass: {
|
|
71
|
+
type: dataTypes.STRING(300)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
is_external: {
|
|
75
|
+
type: dataTypes.SMALLINT,
|
|
76
|
+
default: 0
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @type {column}
|
|
81
|
+
*/
|
|
82
|
+
level: {
|
|
83
|
+
type: 'smallint',
|
|
84
|
+
default: 1,
|
|
85
|
+
validate: v => {
|
|
86
|
+
return v >= 0 && v < 99
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @type {column}
|
|
92
|
+
*/
|
|
93
|
+
email: {
|
|
94
|
+
type: dataTypes.STRING(60),
|
|
95
|
+
default: ''
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
email_state: {
|
|
99
|
+
type: dataTypes.SMALLINT,
|
|
100
|
+
default: 0
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
create_time: {
|
|
104
|
+
type: dataTypes.BIGINT,
|
|
105
|
+
default: 0,
|
|
106
|
+
timestamp: 'insert'
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
update_time: {
|
|
110
|
+
type: dataTypes.BIGINT,
|
|
111
|
+
default: 0,
|
|
112
|
+
timestamp: 'update'
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
failed_count: {
|
|
116
|
+
type: dataTypes.INT,
|
|
117
|
+
default: 0
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
failed_time: {
|
|
121
|
+
type: dataTypes.BIGINT,
|
|
122
|
+
default: 0
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
forbid: {
|
|
126
|
+
type: dataTypes.SMALLINT,
|
|
127
|
+
default: 0
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
is_root: {
|
|
131
|
+
type: dataTypes.SMALLINT,
|
|
132
|
+
default: 0
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
//索引
|
|
137
|
+
index: [
|
|
138
|
+
'create_time',
|
|
139
|
+
'level',
|
|
140
|
+
'is_root'
|
|
141
|
+
],
|
|
142
|
+
|
|
143
|
+
//唯一索引
|
|
144
|
+
unique: [
|
|
145
|
+
'username',
|
|
146
|
+
'email',
|
|
147
|
+
'mobile'
|
|
148
|
+
]
|
|
149
|
+
}
|
|
22
150
|
|
|
23
151
|
// 3. 注册
|
|
24
|
-
db.add(
|
|
152
|
+
db.add(User);
|
|
25
153
|
|
|
26
154
|
;(async () => {
|
|
155
|
+
await db.sync({force: true, debug: true})
|
|
27
156
|
// 插入
|
|
28
|
-
await db.model('User').insert({ name: 'Neo' });
|
|
29
157
|
|
|
30
|
-
|
|
31
|
-
await db.model('User', 'tenant_a').select();
|
|
32
|
-
|
|
33
|
-
// 链式切换
|
|
34
|
-
await db.model('User').schema('tenant_b').where({ age: 20 }).select();
|
|
158
|
+
await db.model('User').insert({username: 'Neo', level: Math.floor((Math.random() * 101))})
|
|
35
159
|
|
|
36
160
|
// 事务
|
|
37
161
|
await db.transaction(async tx => {
|
|
38
|
-
|
|
162
|
+
let data = {
|
|
163
|
+
level: Math.floor(Math.random() * 101),
|
|
164
|
+
info: `age=${Math.floor(Math.random() * 10 + 20)}`
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('update', data)
|
|
168
|
+
await tx.model('User').where(tx.sql`level > 10`).update(data)
|
|
169
|
+
|
|
39
170
|
// 嵌套
|
|
40
|
-
await tx.transaction(async subTx => {
|
|
171
|
+
/* await tx.transaction(async subTx => {
|
|
41
172
|
await subTx.table('logs').insert({ msg: 'log' });
|
|
42
|
-
});
|
|
173
|
+
}); */
|
|
43
174
|
});
|
|
175
|
+
|
|
176
|
+
db.close()
|
|
44
177
|
})();
|