neopg 0.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/index.js +6 -0
- package/lib/ModelChain.js +364 -0
- package/lib/ModelDef.js +204 -0
- package/lib/NeoPG.js +82 -0
- package/lib/SchemaSync.js +506 -0
- package/lib/TransactionScope.js +29 -0
- package/lib/dataTypes.js +60 -0
- package/lib/forbidColumns.js +29 -0
- package/lib/makeId.js +202 -0
- package/lib/makeTimestamp.js +28 -0
- package/lib/randstring.js +23 -0
- package/package.json +28 -0
- package/postgres/bytes.js +78 -0
- package/postgres/connection.js +1042 -0
- package/postgres/errors.js +53 -0
- package/postgres/index.js +566 -0
- package/postgres/large.js +70 -0
- package/postgres/query.js +173 -0
- package/postgres/queue.js +31 -0
- package/postgres/result.js +16 -0
- package/postgres/subscribe.js +277 -0
- package/postgres/types.js +367 -0
- package/test/test-db.js +44 -0
package/index.js
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const makeId = require('./makeId.js');
|
|
4
|
+
const makeTimestamp = require('./makeTimestamp.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ModelChain - 链式查询构建器
|
|
8
|
+
* 负责运行时的查询构建、SQL 拼装和结果处理。
|
|
9
|
+
*/
|
|
10
|
+
class ModelChain {
|
|
11
|
+
/**
|
|
12
|
+
* @param {Object} ctx - 上下文 (NeoPG 实例 或 TransactionScope 实例)
|
|
13
|
+
* @param {ModelDef} def - 模型元数据
|
|
14
|
+
* @param {string} schema - 数据库 schema
|
|
15
|
+
*/
|
|
16
|
+
constructor(ctx, def, schema = 'public') {
|
|
17
|
+
this.ctx = ctx
|
|
18
|
+
this.def = def
|
|
19
|
+
this.sql = ctx.sql
|
|
20
|
+
|
|
21
|
+
this.tableName = def.tableName
|
|
22
|
+
this.schema = schema
|
|
23
|
+
|
|
24
|
+
// --- 查询状态 (AST Lite) ---
|
|
25
|
+
this._conditions = []
|
|
26
|
+
this._order = []
|
|
27
|
+
this._limit = null
|
|
28
|
+
this._offset = null
|
|
29
|
+
this._columns = null
|
|
30
|
+
this._group = null
|
|
31
|
+
this._lock = null
|
|
32
|
+
|
|
33
|
+
// 内部状态标记
|
|
34
|
+
this._isRaw = !!def.isRaw
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --- 静态构造器 ---
|
|
38
|
+
static from(schemaObject) {
|
|
39
|
+
return class AnonymousModel extends ModelChain {
|
|
40
|
+
static schema = schemaObject
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- 核心:链式调用 API ---
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 添加 WHERE 条件
|
|
48
|
+
* 1. .where(sql`age > ${age}`) -> 原生片段 (Query)
|
|
49
|
+
* 2. .where({ a: 1 }) -> a=1
|
|
50
|
+
* 3. .where('age', '>', 18) -> age > 18 (兼容)
|
|
51
|
+
*/
|
|
52
|
+
where(arg1, arg2, arg3) {
|
|
53
|
+
if (!arg1) return this
|
|
54
|
+
|
|
55
|
+
// 1. 识别 Postgres Fragment (Query)
|
|
56
|
+
// 依据指令:直接检测构造函数名称是否为 'Query'
|
|
57
|
+
if (arg1.constructor && arg1.constructor.name === 'Query') {
|
|
58
|
+
this._conditions.push(arg1)
|
|
59
|
+
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
|
+
}
|
|
79
|
+
|
|
80
|
+
return this
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. 字符串/参数写法
|
|
84
|
+
if (typeof arg1 === 'string') {
|
|
85
|
+
// Case A: .where('id', 123) => id = 123 (默认等于)
|
|
86
|
+
if (arg2 !== undefined && arg3 === undefined) {
|
|
87
|
+
this._conditions.push(this.sql`${this.sql(arg1)} = ${arg2}`)
|
|
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}`)
|
|
95
|
+
return this
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Case C: .where('id = ?', 123) (简单兼容)
|
|
99
|
+
if (arg1.includes('?') && arg2 !== undefined) {
|
|
100
|
+
const parts = arg1.split('?')
|
|
101
|
+
|
|
102
|
+
// 只支持单个参数简单替换,复杂的请用 sql``
|
|
103
|
+
if (parts.length === 2) {
|
|
104
|
+
this._conditions.push(this.sql`${this.sql.unsafe(parts[0])}${arg2}${this.sql.unsafe(parts[1])}`)
|
|
105
|
+
return this
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Case D: 纯字符串 (视为 Raw SQL)
|
|
110
|
+
// .where("status = 'active'")
|
|
111
|
+
this._conditions.push(this.sql.unsafe(arg1))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return this
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
whereIf(condition, arg1, arg2, arg3) {
|
|
118
|
+
if (condition) return this.where(arg1, arg2, arg3)
|
|
119
|
+
return this
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
select(columns) {
|
|
123
|
+
if (!columns) return this
|
|
124
|
+
|
|
125
|
+
if (typeof columns === 'string') {
|
|
126
|
+
this._columns = columns.split(',').map(s => s.trim())
|
|
127
|
+
} else if (Array.isArray(columns)) {
|
|
128
|
+
this._columns = columns
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return this
|
|
132
|
+
}
|
|
133
|
+
|
|
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
|
|
142
|
+
|
|
143
|
+
// 1. Fragment
|
|
144
|
+
if (arg1.constructor && arg1.constructor.name === 'Query') {
|
|
145
|
+
this._order.push(arg1)
|
|
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)}`)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return this
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 3. String ('id', 'DESC')
|
|
160
|
+
if (typeof arg1 === 'string') {
|
|
161
|
+
const dir = arg2 ? arg2.toUpperCase() : 'ASC'
|
|
162
|
+
// 检查 arg1 是否包含空格 (e.g. "id desc")
|
|
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)}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return this
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
limit(count, offset = 0) {
|
|
174
|
+
this._limit = count
|
|
175
|
+
this._offset = offset
|
|
176
|
+
return this
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
page(pageIndex, pageSize) {
|
|
180
|
+
return this.limit(pageSize, (pageIndex - 1) * pageSize)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
forUpdate() {
|
|
184
|
+
this._lock = this.sql`FOR UPDATE`
|
|
185
|
+
return this
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
forShare() {
|
|
189
|
+
this._lock = this.sql`FOR SHARE`
|
|
190
|
+
return this
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- 核心:执行方法 ---
|
|
194
|
+
|
|
195
|
+
async find() {
|
|
196
|
+
const tableFragment = this.sql(this.tableName)
|
|
197
|
+
|
|
198
|
+
const colsFragment = this._columns
|
|
199
|
+
? this.sql(this._columns)
|
|
200
|
+
: this.sql`*`
|
|
201
|
+
|
|
202
|
+
const whereFragment = this._conditions.length
|
|
203
|
+
? this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
|
|
204
|
+
: this.sql``
|
|
205
|
+
|
|
206
|
+
const orderFragment = this._order.length
|
|
207
|
+
? this.sql`ORDER BY ${this.sql(this._order)}`
|
|
208
|
+
: this.sql``
|
|
209
|
+
|
|
210
|
+
const limitFragment = this._limit
|
|
211
|
+
? this.sql`LIMIT ${this._limit}`
|
|
212
|
+
: this.sql``
|
|
213
|
+
|
|
214
|
+
const offsetFragment = this._offset
|
|
215
|
+
? this.sql`OFFSET ${this._offset}`
|
|
216
|
+
: this.sql``
|
|
217
|
+
|
|
218
|
+
const lockFragment = this._lock || this.sql``
|
|
219
|
+
|
|
220
|
+
const fullTable = this.sql`${this.sql(this.schema)}.${tableFragment}`
|
|
221
|
+
|
|
222
|
+
return await this.sql`
|
|
223
|
+
SELECT ${colsFragment}
|
|
224
|
+
FROM ${fullTable}
|
|
225
|
+
${whereFragment}
|
|
226
|
+
${orderFragment}
|
|
227
|
+
${limitFragment}
|
|
228
|
+
${offsetFragment}
|
|
229
|
+
${lockFragment}
|
|
230
|
+
`
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Thenable 接口
|
|
234
|
+
then(onFulfilled, onRejected) {
|
|
235
|
+
return this.find().then(onFulfilled, onRejected)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async get() {
|
|
239
|
+
this.limit(1)
|
|
240
|
+
const rows = await this.find()
|
|
241
|
+
return rows.length > 0 ? rows[0] : null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async count() {
|
|
245
|
+
const tableFragment = this.sql(this.tableName);
|
|
246
|
+
const whereFragment = this._conditions.length
|
|
247
|
+
? this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
|
|
248
|
+
: this.sql``
|
|
249
|
+
|
|
250
|
+
const fullTable = this.sql`${this.sql(this.schema)}.${tableFragment}`
|
|
251
|
+
|
|
252
|
+
const result = await this.sql`
|
|
253
|
+
SELECT count(*) as total FROM ${fullTable} ${whereFragment}
|
|
254
|
+
`
|
|
255
|
+
|
|
256
|
+
return parseInt(result[0].total)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- 写入方法 ---
|
|
260
|
+
|
|
261
|
+
async insert(data) {
|
|
262
|
+
const isArray = Array.isArray(data)
|
|
263
|
+
const inputs = isArray ? data : [data]
|
|
264
|
+
if (inputs.length === 0) throw new Error('[NeoPG] Insert data cannot be empty')
|
|
265
|
+
|
|
266
|
+
if (this.def) {
|
|
267
|
+
this._prepareDataForInsert(inputs)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
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
|
+
`
|
|
276
|
+
|
|
277
|
+
if (!isArray && result.length > 0) return result[0]
|
|
278
|
+
|
|
279
|
+
return result
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async update(data) {
|
|
283
|
+
if (!data || Object.keys(data).length === 0) throw new Error('[NeoPG] Update data cannot be empty')
|
|
284
|
+
|
|
285
|
+
if (this.def) {
|
|
286
|
+
this._prepareDataForUpdate(data)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this._conditions.length === 0) {
|
|
290
|
+
throw new Error('[NeoPG] UPDATE requires a WHERE condition')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
|
|
294
|
+
const whereFragment = this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
|
|
295
|
+
|
|
296
|
+
// 使用 sql(data) 自动处理 set a=1, b=2
|
|
297
|
+
return await this.sql`
|
|
298
|
+
UPDATE ${fullTable}
|
|
299
|
+
SET ${this.sql(data)}
|
|
300
|
+
${whereFragment}
|
|
301
|
+
RETURNING *
|
|
302
|
+
`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async delete() {
|
|
306
|
+
if (this._conditions.length === 0) {
|
|
307
|
+
throw new Error('[NeoPG] DELETE requires a WHERE condition')
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const fullTable = this.sql`${this.sql(this.schema)}.${this.sql(this.tableName)}`
|
|
311
|
+
|
|
312
|
+
const whereFragment = this.sql`WHERE ${this.sql(this._conditions, ' AND ')}`
|
|
313
|
+
|
|
314
|
+
return await this.sql`
|
|
315
|
+
DELETE FROM ${fullTable}
|
|
316
|
+
${whereFragment}
|
|
317
|
+
RETURNING *
|
|
318
|
+
`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- 内部辅助方法 ---
|
|
322
|
+
|
|
323
|
+
_prepareDataForInsert(rows) {
|
|
324
|
+
const pk = this.def.primaryKey
|
|
325
|
+
const autoId = this.def.autoId
|
|
326
|
+
const pkLen = this.def.pkLen
|
|
327
|
+
const ts = this.def.timestamps
|
|
328
|
+
const defaults = this.def.defaults
|
|
329
|
+
|
|
330
|
+
let make_timestamp = ts.insert && ts.insert.length > 0
|
|
331
|
+
|
|
332
|
+
for (const row of rows) {
|
|
333
|
+
if (autoId && row[pk] === undefined) {
|
|
334
|
+
row[pk] = this.def.makeId(pkLen)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (make_timestamp) {
|
|
338
|
+
for (const t of ts.insert) {
|
|
339
|
+
makeTimestamp(row, t)
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const key in row) {
|
|
344
|
+
this.def.validateField(key, row[key])
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_prepareDataForUpdate(row) {
|
|
350
|
+
const ts = this.def.timestamps
|
|
351
|
+
|
|
352
|
+
if (ts.update && ts.update.length > 0) {
|
|
353
|
+
for (const t of ts.update) {
|
|
354
|
+
makeTimestamp(row, t)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
for (const key in row) {
|
|
359
|
+
this.def.validateField(key, row[key])
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
module.exports = ModelChain
|
package/lib/ModelDef.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const dataTypes = require('./dataTypes.js')
|
|
4
|
+
const makeId = require('./makeId.js')
|
|
5
|
+
const forbidColumns = require('./forbidColumns.js')
|
|
6
|
+
|
|
7
|
+
class ModelDef {
|
|
8
|
+
constructor(rawSchema) {
|
|
9
|
+
this.tableName = rawSchema.tableName
|
|
10
|
+
this.modelName = rawSchema.modelName || rawSchema.tableName
|
|
11
|
+
this.primaryKey = rawSchema.primaryKey || 'id'
|
|
12
|
+
this.columns = rawSchema.column || {}
|
|
13
|
+
|
|
14
|
+
this._parseColumns()
|
|
15
|
+
|
|
16
|
+
// 1. 解析主键相关信息 (pkLen, autoId)
|
|
17
|
+
const pkInfo = this._parsePkInfo(rawSchema)
|
|
18
|
+
this.pkLen = pkInfo.len
|
|
19
|
+
this.autoId = pkInfo.auto
|
|
20
|
+
this.makeId = makeId.serialId
|
|
21
|
+
|
|
22
|
+
// 2. 时间戳配置
|
|
23
|
+
this.timestamps = this._parseTimestamps()
|
|
24
|
+
|
|
25
|
+
// 3. 静态默认值
|
|
26
|
+
this.defaults = this._parseDefaults()
|
|
27
|
+
|
|
28
|
+
this.validates = Object.create(null)
|
|
29
|
+
this._parseValidate()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
*
|
|
34
|
+
*/
|
|
35
|
+
_parseValidate() {
|
|
36
|
+
for (let k in this.columns) {
|
|
37
|
+
let col = this.columns[k]
|
|
38
|
+
if (col.validate) {
|
|
39
|
+
if (col.validate instanceof RegExp) {
|
|
40
|
+
this.validates[k] = (v) => {
|
|
41
|
+
return col.validate.test(v)
|
|
42
|
+
}
|
|
43
|
+
} else if (Array.isArray(col.validate)) {
|
|
44
|
+
this.validates[k] = (v) => {
|
|
45
|
+
return col.validate.includes(v)
|
|
46
|
+
}
|
|
47
|
+
} else if (typeof col.validate === 'function') {
|
|
48
|
+
this.validates[k] = col.validate
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_parseColumns() {
|
|
55
|
+
for (let k in this.columns) {
|
|
56
|
+
if (forbidColumns.forbid.includes(k.toLowerCase().trim())) {
|
|
57
|
+
throw new Error(`[NeoPG] Column name '${k}' in table '${this.tableName}' is FORBIDDEN.`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!/^[a-z][a-z0-9_]*$/i.test(colName)) {
|
|
61
|
+
throw new Error(`[NeoPG] Column name '${k}' is invalid. Only alphanumeric and underscore allowed, must start with letter.`)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 解析主键策略
|
|
68
|
+
* 产出: { len: Number, auto: Boolean }
|
|
69
|
+
*/
|
|
70
|
+
_parsePkInfo(schema) {
|
|
71
|
+
const pk = this.primaryKey
|
|
72
|
+
const colDef = this.columns[pk]
|
|
73
|
+
|
|
74
|
+
// 默认值
|
|
75
|
+
let info = { len: 16, auto: true, type: 'string'}
|
|
76
|
+
|
|
77
|
+
// 如果 Schema 显式关闭,优先级最高
|
|
78
|
+
if (schema.autoId === false) {
|
|
79
|
+
info.auto = false
|
|
80
|
+
return info
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 如果没有主键定义,无法自动生成
|
|
84
|
+
if (!colDef) {
|
|
85
|
+
info.auto = false
|
|
86
|
+
return info
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 显式指定了 autoIncrement (数据库自增)
|
|
90
|
+
if (colDef.autoIncrement) {
|
|
91
|
+
info.auto = false
|
|
92
|
+
return info
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// --- 类型分析 ---
|
|
96
|
+
let typeStr = ''
|
|
97
|
+
|
|
98
|
+
if (typeof colDef.type === 'string') {
|
|
99
|
+
typeStr = colDef.type.toLowerCase().trim()
|
|
100
|
+
} else {
|
|
101
|
+
// 容错:如果 type 是 undefined
|
|
102
|
+
info.auto = false
|
|
103
|
+
return info
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A. 数据库自增类型 (serial, bigserial, integer/int 且未声明 autoId=true)
|
|
107
|
+
if (typeStr.includes('serial')) {
|
|
108
|
+
info.auto = false
|
|
109
|
+
return info
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeStr === 'bigint') {
|
|
113
|
+
info.auto = true
|
|
114
|
+
info.type = 'number'
|
|
115
|
+
this.makeId = makeId.bigId
|
|
116
|
+
return info
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// B. 字符串类型处理 (varchar, char, string, text)
|
|
120
|
+
if (typeStr.includes('char') || typeStr.includes('varchar') || typeStr.includes('text')) {
|
|
121
|
+
// 1. 尝试解析长度: varchar(32) -> 32
|
|
122
|
+
const match = typeStr.match(/\((\d+)\)/)
|
|
123
|
+
if (match && match[1]) {
|
|
124
|
+
info.len = parseInt(match[1], 10)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. 如果 Schema 根部定义了 pkLen,覆盖解析值
|
|
128
|
+
if (schema.pkLen && typeof schema.pkLen === 'number') {
|
|
129
|
+
if (info.len < schema.pkLen) {
|
|
130
|
+
info.len = schema.pkLen
|
|
131
|
+
} else if (info.len > schema.pkLen) {
|
|
132
|
+
schema.pkLen = info.len
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
schema.pkLen = info.len
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
info.auto = true
|
|
139
|
+
} else {
|
|
140
|
+
// 其他类型(如 int 但不是 serial),通常不自动生成字符串ID
|
|
141
|
+
// 除非用户在 Schema 显式写了 autoId: true,否则偏向于 false
|
|
142
|
+
if (schema.autoId !== true) {
|
|
143
|
+
info.auto = false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return info
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_parseTimestamps() {
|
|
151
|
+
const insertTs = []
|
|
152
|
+
const updateTs = []
|
|
153
|
+
|
|
154
|
+
for (const [colName, colDef] of Object.entries(this.columns)) {
|
|
155
|
+
if (!colDef.timestamp) continue
|
|
156
|
+
|
|
157
|
+
let typeStr = 'bigint'
|
|
158
|
+
|
|
159
|
+
if (typeof colDef.type === 'string') {
|
|
160
|
+
const t = colDef.type.toLowerCase()
|
|
161
|
+
if (t.includes('int') && !t.includes('big')) typeStr = 'int'
|
|
162
|
+
else if (t.includes('timestamp') || t.includes('date')) typeStr = 'timestamp'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const tuple = [colName, typeStr]
|
|
166
|
+
|
|
167
|
+
if (colDef.timestamp === 'insert' || colDef.timestamp === true) {
|
|
168
|
+
insertTs.push(tuple)
|
|
169
|
+
} else if (colDef.timestamp === 'update') {
|
|
170
|
+
insertTs.push(tuple)
|
|
171
|
+
updateTs.push(tuple)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { insert: insertTs, update: updateTs }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_parseDefaults() {
|
|
179
|
+
const defs = []
|
|
180
|
+
for (const [colName, colDef] of Object.entries(this.columns)) {
|
|
181
|
+
if (colDef.default !== undefined) {
|
|
182
|
+
defs.push({ key: colName, val: colDef.default })
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return defs
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
validateField(key, value) {
|
|
189
|
+
const col_validate = this.validates[key]
|
|
190
|
+
if (!col_validate) return true
|
|
191
|
+
|
|
192
|
+
if (value === undefined) {
|
|
193
|
+
throw new Error(`[NeoPG] Field '${key}' is required.`)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!col_validate(value)) {
|
|
197
|
+
throw new Error(`[NeoPG] Validation failed for field '${key}' with value: ${value}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = ModelDef
|
package/lib/NeoPG.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const postgres = require('../postgres/index.js')
|
|
4
|
+
const ModelDef = require('./ModelDef.js')
|
|
5
|
+
const SchemaSync = require('./SchemaSync.js')
|
|
6
|
+
const TransactionScope = require('./TransactionScope.js')
|
|
7
|
+
const ModelChain = require('./ModelChain.js')
|
|
8
|
+
const dataTypes = require('./dataTypes.js')
|
|
9
|
+
|
|
10
|
+
class NeoPG {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.driver = postgres(config)
|
|
13
|
+
this.ModelChain = ModelChain
|
|
14
|
+
this.sql = this.driver
|
|
15
|
+
|
|
16
|
+
this.defaultSchema = config.schema || 'public'
|
|
17
|
+
this.registry = new Map()
|
|
18
|
+
this.config = config
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
table(tableName, schema = null) {
|
|
22
|
+
const target = schema || this.defaultSchema
|
|
23
|
+
return new this.ModelChain(this.driver, {tableName, isRaw: true}, target)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
model(name, schema = null) {
|
|
27
|
+
const item = this.registry.get(name)
|
|
28
|
+
if (!item) throw new Error(`[NeoPG] Model '${name}' not found.`)
|
|
29
|
+
|
|
30
|
+
const target = schema || this.defaultSchema
|
|
31
|
+
return new item.Class(this.driver, item.def, target)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- 注册 ---
|
|
35
|
+
|
|
36
|
+
add(input) {
|
|
37
|
+
let ModelClass;
|
|
38
|
+
if (typeof input === 'function') {
|
|
39
|
+
ModelClass = input
|
|
40
|
+
} else {
|
|
41
|
+
ModelClass = this.ModelChain.from(input)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rawSchema = ModelClass.schema
|
|
45
|
+
if (!rawSchema) throw new Error(`[NeoPG] Missing static schema for ${ModelClass.name}`)
|
|
46
|
+
|
|
47
|
+
const def = new ModelDef(rawSchema)
|
|
48
|
+
|
|
49
|
+
this.registry.set(def.modelName, {
|
|
50
|
+
Class: ModelClass,
|
|
51
|
+
def: def
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return this
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- 事务 ---
|
|
58
|
+
async transaction(callback) {
|
|
59
|
+
return await this.driver.begin(async (trxSql) => {
|
|
60
|
+
const scope = new TransactionScope(this, trxSql)
|
|
61
|
+
return await callback(scope)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- 同步 ---
|
|
66
|
+
async sync(options = {}) {
|
|
67
|
+
for (const item of this.registry.values()) {
|
|
68
|
+
await SchemaSync.execute(this.driver, item.def, this, options)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async close() {
|
|
73
|
+
await this.driver.end()
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
NeoPG.dataTypes = dataTypes
|
|
78
|
+
NeoPG.ModelChain = ModelChain
|
|
79
|
+
NeoPG.postgres = postgres
|
|
80
|
+
NeoPG.SchemaSync = SchemaSync
|
|
81
|
+
|
|
82
|
+
module.exports = NeoPG
|