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 ADDED
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ const NeoPG = require('./lib/NeoPG.js')
4
+
5
+ module.exports = NeoPG
6
+
@@ -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
@@ -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