weifuwu 0.27.2 → 0.27.3
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/dist/ai/provider.d.ts +45 -0
- package/dist/ai/stream.d.ts +13 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +131 -0
- package/dist/core/cookie.d.ts +36 -0
- package/dist/core/env.d.ts +69 -0
- package/dist/core/logger.d.ts +16 -0
- package/dist/core/router.d.ts +72 -0
- package/dist/core/serve.d.ts +38 -0
- package/dist/core/sse.d.ts +47 -0
- package/dist/core/trace.d.ts +95 -0
- package/dist/graphql.d.ts +16 -0
- package/dist/hub.d.ts +36 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +3963 -0
- package/dist/mailer.d.ts +51 -0
- package/dist/middleware/compress.d.ts +20 -0
- package/dist/middleware/cors.d.ts +25 -0
- package/dist/middleware/csrf.d.ts +47 -0
- package/dist/middleware/flash.d.ts +90 -0
- package/dist/middleware/health.d.ts +24 -0
- package/dist/middleware/helmet.d.ts +33 -0
- package/dist/middleware/i18n.d.ts +39 -0
- package/dist/middleware/rate-limit.d.ts +44 -0
- package/dist/middleware/request-id.d.ts +40 -0
- package/dist/middleware/static.d.ts +23 -0
- package/dist/middleware/theme.d.ts +31 -0
- package/dist/middleware/upload.d.ts +55 -0
- package/dist/middleware/validate.d.ts +32 -0
- package/dist/postgres/client.d.ts +4 -0
- package/dist/postgres/index.d.ts +4 -0
- package/dist/postgres/module.d.ts +16 -0
- package/dist/postgres/schema/columns.d.ts +99 -0
- package/dist/postgres/schema/index.d.ts +6 -0
- package/dist/postgres/schema/sql.d.ts +22 -0
- package/dist/postgres/schema/table.d.ts +141 -0
- package/dist/postgres/schema/where.d.ts +29 -0
- package/dist/postgres/types.d.ts +49 -0
- package/dist/queue/cron.d.ts +9 -0
- package/dist/queue/index.d.ts +2 -0
- package/dist/queue/types.d.ts +61 -0
- package/dist/redis/client.d.ts +2 -0
- package/{redis/index.ts → dist/redis/index.d.ts} +2 -2
- package/dist/redis/types.d.ts +17 -0
- package/dist/test/test-utils.d.ts +193 -0
- package/dist/types.d.ts +50 -0
- package/package.json +10 -10
- package/ai/provider.ts +0 -129
- package/ai/stream.ts +0 -63
- package/cli.ts +0 -147
- package/core/cookie.ts +0 -114
- package/core/env.ts +0 -142
- package/core/logger.ts +0 -72
- package/core/router.ts +0 -795
- package/core/serve.ts +0 -294
- package/core/sse.ts +0 -85
- package/core/trace.ts +0 -146
- package/graphql.ts +0 -267
- package/hub.ts +0 -133
- package/index.ts +0 -71
- package/mailer.ts +0 -81
- package/middleware/compress.ts +0 -103
- package/middleware/cors.ts +0 -81
- package/middleware/csrf.ts +0 -112
- package/middleware/flash.ts +0 -144
- package/middleware/health.ts +0 -44
- package/middleware/helmet.ts +0 -98
- package/middleware/i18n.ts +0 -175
- package/middleware/rate-limit.ts +0 -167
- package/middleware/request-id.ts +0 -60
- package/middleware/static.ts +0 -149
- package/middleware/theme.ts +0 -84
- package/middleware/upload.ts +0 -168
- package/middleware/validate.ts +0 -186
- package/postgres/client.ts +0 -132
- package/postgres/index.ts +0 -4
- package/postgres/module.ts +0 -37
- package/postgres/schema/columns.ts +0 -186
- package/postgres/schema/index.ts +0 -36
- package/postgres/schema/sql.ts +0 -39
- package/postgres/schema/table.ts +0 -548
- package/postgres/schema/where.ts +0 -99
- package/postgres/types.ts +0 -48
- package/queue/cron.ts +0 -90
- package/queue/index.ts +0 -654
- package/queue/types.ts +0 -60
- package/redis/client.ts +0 -24
- package/redis/types.ts +0 -28
- package/types.ts +0 -78
package/postgres/schema/table.ts
DELETED
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import type { SqlClient } from '../../types.ts'
|
|
3
|
-
import { ColumnBuilder, toDDL, type PartitionByDef } from './columns.ts'
|
|
4
|
-
import { SQL } from './sql.ts'
|
|
5
|
-
import { and } from './where.ts'
|
|
6
|
-
|
|
7
|
-
/** Options for table index creation. */
|
|
8
|
-
export interface IndexOptions {
|
|
9
|
-
/** Whether the index should be UNIQUE. */
|
|
10
|
-
unique?: boolean
|
|
11
|
-
/** Index type: btree (default), hnsw (pgvector), gin (JSONB). */
|
|
12
|
-
type?: 'btree' | 'hnsw' | 'gin'
|
|
13
|
-
/** Create index in DESC order. */
|
|
14
|
-
desc?: boolean
|
|
15
|
-
/** Custom operator class (e.g. `vector_cosine_ops`). */
|
|
16
|
-
operator?: string
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/** Options for CREATE TABLE. */
|
|
20
|
-
export interface CreateOptions {
|
|
21
|
-
/** Partition by clause (RANGE, LIST, or HASH). */
|
|
22
|
-
partitionBy?: PartitionByDef
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Options for find/read queries. */
|
|
26
|
-
export interface FindOptions {
|
|
27
|
-
/** ORDER BY clause: `{ column: 'asc' | 'desc' }`. */
|
|
28
|
-
orderBy?: Record<string, 'asc' | 'desc'>
|
|
29
|
-
/** LIMIT. */
|
|
30
|
-
limit?: number
|
|
31
|
-
/** OFFSET. */
|
|
32
|
-
offset?: number
|
|
33
|
-
/** Columns to SELECT (default: all). */
|
|
34
|
-
select?: string[]
|
|
35
|
-
/** Include soft-deleted rows (also sets `withDeleted` context). */
|
|
36
|
-
withDeleted?: boolean
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ColEntry {
|
|
40
|
-
prop: string
|
|
41
|
-
db: string
|
|
42
|
-
auto: boolean
|
|
43
|
-
column: ColumnBuilder<unknown>
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Type-safe table schema + CRUD operations.
|
|
48
|
-
*
|
|
49
|
-
* Create an instance with {@link pgTable}, then call `.bind(sql)` to get a
|
|
50
|
-
* `BoundTable` for running queries.
|
|
51
|
-
*
|
|
52
|
-
* ```ts
|
|
53
|
-
* const users = pgTable('users', {
|
|
54
|
-
* id: serial('id').primaryKey(),
|
|
55
|
-
* name: text('name').notNull(),
|
|
56
|
-
* email: text('email').unique(),
|
|
57
|
-
* })
|
|
58
|
-
*
|
|
59
|
-
* const db = users.bind(sql)
|
|
60
|
-
* await db.create()
|
|
61
|
-
* await db.insert({ name: 'Alice', email: 'a@b.com' })
|
|
62
|
-
* const row = await db.findBy({ email: 'a@b.com' })
|
|
63
|
-
* ```
|
|
64
|
-
*/
|
|
65
|
-
export class Table<R extends Record<string, unknown>> {
|
|
66
|
-
/** Database table name. */
|
|
67
|
-
readonly tableName: string
|
|
68
|
-
/** All column builders (order-preserving). */
|
|
69
|
-
readonly columns: ColumnBuilder<unknown>[]
|
|
70
|
-
/** Column builders keyed by property name. */
|
|
71
|
-
readonly builders: Record<string, ColumnBuilder<unknown>>
|
|
72
|
-
private colEntries: ColEntry[]
|
|
73
|
-
|
|
74
|
-
constructor(tableName: string, builders: Record<string, ColumnBuilder<unknown>>) {
|
|
75
|
-
this.tableName = tableName
|
|
76
|
-
this.builders = builders
|
|
77
|
-
this.columns = Object.values(builders)
|
|
78
|
-
this.colEntries = Object.entries(builders).map(([prop, col]) => ({
|
|
79
|
-
prop,
|
|
80
|
-
db: col.name,
|
|
81
|
-
auto: col.isAutoGenerate,
|
|
82
|
-
column: col,
|
|
83
|
-
}))
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Check if the table has a column with the given DB name. */
|
|
87
|
-
hasColumn(dbName: string): boolean {
|
|
88
|
-
return this.colEntries.some((e) => e.db === dbName)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Bind this table schema to a SQL connection, returning a `BoundTable`
|
|
93
|
-
* that can run queries without passing `sql` to every call.
|
|
94
|
-
*/
|
|
95
|
-
bind(sql: SqlClient): BoundTable<R> {
|
|
96
|
-
return new BoundTable(sql, this.tableName, this.builders)
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Returns the primary key column name (DB name), or 'id' as fallback. */
|
|
100
|
-
private get pkColumn(): string {
|
|
101
|
-
const entry = this.colEntries.find((e) => e.column.isPrimaryKey)
|
|
102
|
-
return entry ? entry.db : 'id'
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Adds `deleted_at IS NULL` condition if the table has soft delete and not explicitly excluded. */
|
|
106
|
-
private _softDeleteFilter(where: unknown, opts?: FindOptions): string | null {
|
|
107
|
-
if (!this.hasColumn('deleted_at')) return null
|
|
108
|
-
if (opts?.withDeleted) return null
|
|
109
|
-
// If user explicitly filters by deleted_at, don't add our own
|
|
110
|
-
if (where && typeof where === 'object' && !Array.isArray(where) && !(where instanceof SQL)) {
|
|
111
|
-
if ('deleted_at' in (where as Record<string, unknown>)) return null
|
|
112
|
-
}
|
|
113
|
-
return '"deleted_at" IS NULL'
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async create(sql: SqlClient, opts?: CreateOptions): Promise<void> {
|
|
117
|
-
const colDDL = this.columns.map(toDDL)
|
|
118
|
-
let ddl = `CREATE TABLE IF NOT EXISTS "${this.tableName}" (\n ${colDDL.join(',\n ')}\n)`
|
|
119
|
-
if (opts?.partitionBy) {
|
|
120
|
-
ddl += ` PARTITION BY ${opts.partitionBy.type} ("${opts.partitionBy.column}")`
|
|
121
|
-
}
|
|
122
|
-
await sql.unsafe(ddl)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
async drop(sql: SqlClient, opts?: { cascade?: boolean }): Promise<void> {
|
|
126
|
-
const cascade = opts?.cascade ? ' CASCADE' : ''
|
|
127
|
-
await sql.unsafe(`DROP TABLE IF EXISTS "${this.tableName}"${cascade}`)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
async createIndex(
|
|
131
|
-
sql: SqlClient,
|
|
132
|
-
columns: string | string[],
|
|
133
|
-
opts?: IndexOptions,
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
const cols = Array.isArray(columns) ? columns : [columns]
|
|
136
|
-
const name = `"${this.tableName}_${cols.join('_')}${opts?.unique ? '_uidx' : '_idx'}"`
|
|
137
|
-
const unique = opts?.unique ? 'UNIQUE' : ''
|
|
138
|
-
const using = opts?.type ? `USING ${opts.type.toUpperCase()}` : ''
|
|
139
|
-
const colList = cols.map((c) => (opts?.desc ? `"${c}" DESC` : `"${c}"`)).join(', ')
|
|
140
|
-
const operator = opts?.operator ? ` ${opts.operator}` : ''
|
|
141
|
-
|
|
142
|
-
const ddl =
|
|
143
|
-
`CREATE ${unique} INDEX IF NOT EXISTS ${name} ON "${this.tableName}" ${using} (${colList}${operator})`.replace(
|
|
144
|
-
/\s+/g,
|
|
145
|
-
' ',
|
|
146
|
-
)
|
|
147
|
-
await sql.unsafe(ddl)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async createUniqueIndex(sql: SqlClient, columns: string | string[]): Promise<void> {
|
|
151
|
-
await this.createIndex(sql, columns, { unique: true })
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// --- Private helpers ---
|
|
155
|
-
|
|
156
|
-
private _buildConditions(
|
|
157
|
-
where: Partial<R> | SQL | SQL[] | undefined,
|
|
158
|
-
startIndex: number,
|
|
159
|
-
): { conditions: string[]; values: unknown[] } {
|
|
160
|
-
const conditions: string[] = []
|
|
161
|
-
const values: unknown[] = []
|
|
162
|
-
|
|
163
|
-
let w = where
|
|
164
|
-
if (Array.isArray(w)) {
|
|
165
|
-
w = w.length > 0 ? and(...w) : undefined
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (w instanceof SQL) {
|
|
169
|
-
let fragment = ''
|
|
170
|
-
for (let i = 0; i < w.strings.length; i++) {
|
|
171
|
-
fragment += w.strings[i]
|
|
172
|
-
if (i < w.values.length) {
|
|
173
|
-
fragment += `$${startIndex + values.length + 1}`
|
|
174
|
-
values.push(w.values[i])
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
conditions.push(fragment)
|
|
178
|
-
} else {
|
|
179
|
-
for (const [prop, value] of Object.entries(w || {}) as [string, unknown][]) {
|
|
180
|
-
if (value === undefined) continue
|
|
181
|
-
const entry = this.colEntries.find((e) => e.prop === prop)
|
|
182
|
-
const db = entry ? entry.db : prop
|
|
183
|
-
conditions.push(`"${db}" = $${startIndex + values.length + 1}`)
|
|
184
|
-
values.push(value)
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return { conditions, values }
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private _buildSET(data: Partial<R>): { sets: string[]; values: unknown[] } {
|
|
192
|
-
const sets: string[] = []
|
|
193
|
-
const values: unknown[] = []
|
|
194
|
-
|
|
195
|
-
const d = data as Record<string, unknown>
|
|
196
|
-
for (const { prop, db } of this.colEntries) {
|
|
197
|
-
if (prop in d && d[prop] !== undefined) {
|
|
198
|
-
const val = d[prop]
|
|
199
|
-
if (val instanceof SQL) {
|
|
200
|
-
sets.push(`"${db}" = ${val.toSQL()}`)
|
|
201
|
-
} else {
|
|
202
|
-
sets.push(`"${db}" = $${sets.length + 1}`)
|
|
203
|
-
values.push(val)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (this.hasColumn('updated_at') && !d.updated_at) {
|
|
209
|
-
sets.push('"updated_at" = NOW()')
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
return { sets, values }
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// --- CRUD ---
|
|
216
|
-
|
|
217
|
-
async insert(sql: SqlClient, data: Partial<R>): Promise<R> {
|
|
218
|
-
const filtered: Record<string, unknown> = {}
|
|
219
|
-
for (const { prop, db, auto } of this.colEntries) {
|
|
220
|
-
if (auto) continue
|
|
221
|
-
const val = (data as Record<string, unknown>)[prop]
|
|
222
|
-
if (val !== undefined) {
|
|
223
|
-
filtered[db] = val
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
const [row] = await sql`
|
|
227
|
-
INSERT INTO ${sql(this.tableName as string)} ${sql(filtered as any)} RETURNING *
|
|
228
|
-
`
|
|
229
|
-
return row as unknown as R
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async insertMany(sql: SqlClient, data: Partial<R>[]): Promise<R[]> {
|
|
233
|
-
const filtered: Record<string, unknown>[] = []
|
|
234
|
-
for (const item of data) {
|
|
235
|
-
const row: Record<string, unknown> = {}
|
|
236
|
-
for (const { prop, db, auto } of this.colEntries) {
|
|
237
|
-
if (auto) continue
|
|
238
|
-
const val = (item as Record<string, unknown>)[prop]
|
|
239
|
-
if (val !== undefined) {
|
|
240
|
-
row[db] = val
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
filtered.push(row)
|
|
244
|
-
}
|
|
245
|
-
const rows = await sql`
|
|
246
|
-
INSERT INTO ${sql(this.tableName as string)} ${sql(filtered as any)} RETURNING *
|
|
247
|
-
`
|
|
248
|
-
return rows as unknown as R[]
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async read(
|
|
252
|
-
sql: SqlClient,
|
|
253
|
-
id: string | number,
|
|
254
|
-
opts?: Pick<FindOptions, 'select' | 'withDeleted'>,
|
|
255
|
-
): Promise<R | undefined> {
|
|
256
|
-
const pk = this.pkColumn
|
|
257
|
-
const columns = opts?.select?.length ? opts.select.map((c) => `"${c}"`).join(', ') : '*'
|
|
258
|
-
const softDel = this._softDeleteFilter(null, opts)
|
|
259
|
-
const extraAnd = softDel ? ` AND ${softDel}` : ''
|
|
260
|
-
|
|
261
|
-
const [row] = await sql.unsafe(
|
|
262
|
-
`SELECT ${columns} FROM "${this.tableName}" WHERE "${pk}" = $1${extraAnd} LIMIT 1`,
|
|
263
|
-
[id],
|
|
264
|
-
)
|
|
265
|
-
return (row as unknown as R) ?? undefined
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async readMany(
|
|
269
|
-
sql: SqlClient,
|
|
270
|
-
where?: Partial<R> | SQL | SQL[],
|
|
271
|
-
opts?: FindOptions,
|
|
272
|
-
): Promise<{ count: number; data: R[] }> {
|
|
273
|
-
const { conditions, values } = this._buildConditions(where, 0)
|
|
274
|
-
|
|
275
|
-
const softDel = this._softDeleteFilter(where, opts)
|
|
276
|
-
if (softDel) conditions.push(softDel)
|
|
277
|
-
|
|
278
|
-
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''
|
|
279
|
-
|
|
280
|
-
const [countRow] = await sql.unsafe(
|
|
281
|
-
`SELECT COUNT(*) AS _total FROM "${this.tableName}"${whereClause}`,
|
|
282
|
-
values as any[],
|
|
283
|
-
)
|
|
284
|
-
const count = Number((countRow as Record<string, number>)._total)
|
|
285
|
-
|
|
286
|
-
if (
|
|
287
|
-
conditions.length === 0 &&
|
|
288
|
-
!opts?.orderBy &&
|
|
289
|
-
!opts?.limit &&
|
|
290
|
-
!opts?.offset &&
|
|
291
|
-
!opts?.select
|
|
292
|
-
) {
|
|
293
|
-
const rows = await sql`SELECT * FROM ${sql(this.tableName as string)}`
|
|
294
|
-
return { count, data: rows as unknown as R[] }
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const columns = opts?.select?.length ? opts.select.map((c) => `"${c}"`).join(', ') : '*'
|
|
298
|
-
let query = `SELECT ${columns} FROM "${this.tableName}"${whereClause}`
|
|
299
|
-
if (opts?.orderBy) {
|
|
300
|
-
const orders = Object.entries(opts.orderBy)
|
|
301
|
-
.map(([prop, dir]) => {
|
|
302
|
-
const entry = this.colEntries.find((e) => e.prop === prop)
|
|
303
|
-
return `"${entry?.db || prop}" ${dir.toUpperCase()}`
|
|
304
|
-
})
|
|
305
|
-
.join(', ')
|
|
306
|
-
query += ` ORDER BY ${orders}`
|
|
307
|
-
}
|
|
308
|
-
if (opts?.limit) query += ` LIMIT ${opts.limit}`
|
|
309
|
-
if (opts?.offset) query += ` OFFSET ${opts.offset}`
|
|
310
|
-
|
|
311
|
-
const rows = await sql.unsafe(query, values as any[])
|
|
312
|
-
return { count, data: rows as unknown as R[] }
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async update(sql: SqlClient, id: string | number, data: Partial<R>): Promise<R | undefined> {
|
|
316
|
-
const { sets, values: setValues } = this._buildSET(data)
|
|
317
|
-
if (sets.length === 0) return undefined
|
|
318
|
-
|
|
319
|
-
const pk = this.pkColumn
|
|
320
|
-
const [row] = await sql.unsafe(
|
|
321
|
-
`UPDATE "${this.tableName}" SET ${sets.join(', ')} WHERE "${pk}" = $${setValues.length + 1} RETURNING *`,
|
|
322
|
-
[...setValues, id] as any[],
|
|
323
|
-
)
|
|
324
|
-
return (row as unknown as R) ?? undefined
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async updateMany(
|
|
328
|
-
sql: SqlClient,
|
|
329
|
-
where: Partial<R> | SQL | SQL[],
|
|
330
|
-
data: Partial<R>,
|
|
331
|
-
): Promise<number> {
|
|
332
|
-
const { sets, values: setValues } = this._buildSET(data)
|
|
333
|
-
if (sets.length === 0) return 0
|
|
334
|
-
|
|
335
|
-
const { conditions: wConditions, values: wValues } = this._buildConditions(
|
|
336
|
-
where,
|
|
337
|
-
setValues.length,
|
|
338
|
-
)
|
|
339
|
-
if (wConditions.length === 0) return 0
|
|
340
|
-
|
|
341
|
-
const rows = await sql.unsafe(
|
|
342
|
-
`UPDATE "${this.tableName}" SET ${sets.join(', ')} WHERE ${wConditions.join(' AND ')} RETURNING 1`,
|
|
343
|
-
[...setValues, ...wValues] as any[],
|
|
344
|
-
)
|
|
345
|
-
return rows.length
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async delete(sql: SqlClient, id: string | number): Promise<R | undefined> {
|
|
349
|
-
const pk = this.pkColumn
|
|
350
|
-
if (this.hasColumn('deleted_at')) {
|
|
351
|
-
const [row] = await sql.unsafe(
|
|
352
|
-
`UPDATE "${this.tableName}" SET "deleted_at" = NOW() WHERE "${pk}" = $1 RETURNING *`,
|
|
353
|
-
[id],
|
|
354
|
-
)
|
|
355
|
-
return (row as unknown as R) ?? undefined
|
|
356
|
-
}
|
|
357
|
-
const [row] = await sql.unsafe(
|
|
358
|
-
`DELETE FROM "${this.tableName}" WHERE "${pk}" = $1 RETURNING *`,
|
|
359
|
-
[id],
|
|
360
|
-
)
|
|
361
|
-
return (row as unknown as R) ?? undefined
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
async hardDelete(sql: SqlClient, id: string | number): Promise<R | undefined> {
|
|
365
|
-
const pk = this.pkColumn
|
|
366
|
-
const [row] = await sql.unsafe(
|
|
367
|
-
`DELETE FROM "${this.tableName}" WHERE "${pk}" = $1 RETURNING *`,
|
|
368
|
-
[id],
|
|
369
|
-
)
|
|
370
|
-
return (row as unknown as R) ?? undefined
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async deleteMany(sql: SqlClient, where: Partial<R> | SQL | SQL[]): Promise<number> {
|
|
374
|
-
const { conditions, values } = this._buildConditions(where, 0)
|
|
375
|
-
if (conditions.length === 0) return 0
|
|
376
|
-
|
|
377
|
-
if (this.hasColumn('deleted_at')) {
|
|
378
|
-
const rows = await sql.unsafe(
|
|
379
|
-
`UPDATE "${this.tableName}" SET "deleted_at" = NOW() WHERE ${conditions.join(' AND ')} RETURNING 1`,
|
|
380
|
-
values as any[],
|
|
381
|
-
)
|
|
382
|
-
return rows.length
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
const rows = await sql.unsafe(
|
|
386
|
-
`DELETE FROM "${this.tableName}" WHERE ${conditions.join(' AND ')} RETURNING 1`,
|
|
387
|
-
values as any[],
|
|
388
|
-
)
|
|
389
|
-
return rows.length
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async hardDeleteMany(sql: SqlClient, where: Partial<R> | SQL | SQL[]): Promise<number> {
|
|
393
|
-
const { conditions, values } = this._buildConditions(where, 0)
|
|
394
|
-
if (conditions.length === 0) return 0
|
|
395
|
-
|
|
396
|
-
const rows = await sql.unsafe(
|
|
397
|
-
`DELETE FROM "${this.tableName}" WHERE ${conditions.join(' AND ')} RETURNING 1`,
|
|
398
|
-
values as any[],
|
|
399
|
-
)
|
|
400
|
-
return rows.length
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async upsert(sql: SqlClient, data: Partial<R>, conflict: string | string[]): Promise<R> {
|
|
404
|
-
const filtered: Record<string, unknown> = {}
|
|
405
|
-
for (const { prop, db, auto } of this.colEntries) {
|
|
406
|
-
if (auto) continue
|
|
407
|
-
const val = (data as Record<string, unknown>)[prop]
|
|
408
|
-
if (val !== undefined) {
|
|
409
|
-
filtered[db] = val
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const keys = Object.keys(filtered)
|
|
414
|
-
if (keys.length === 0) throw new Error('upsert: no data to insert')
|
|
415
|
-
|
|
416
|
-
const conflictCols = Array.isArray(conflict) ? conflict : [conflict]
|
|
417
|
-
const dbCols = keys.map((c) => `"${c}"`)
|
|
418
|
-
const placeholders = keys.map((_, i) => `$${i + 1}`)
|
|
419
|
-
const updateSet = keys
|
|
420
|
-
.filter((k) => !conflictCols.includes(k))
|
|
421
|
-
.map((k) => `"${k}" = EXCLUDED."${k}"`)
|
|
422
|
-
.join(', ')
|
|
423
|
-
|
|
424
|
-
if (!updateSet) {
|
|
425
|
-
const [row] = await sql.unsafe(
|
|
426
|
-
`INSERT INTO "${this.tableName}" (${dbCols.join(', ')}) VALUES (${placeholders.join(', ')}) ON CONFLICT (${conflictCols.map((c) => `"${c}"`).join(', ')}) DO NOTHING RETURNING *`,
|
|
427
|
-
Object.values(filtered) as any[],
|
|
428
|
-
)
|
|
429
|
-
return (row as unknown as R) ?? (undefined as unknown as R)
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const [row] = await sql.unsafe(
|
|
433
|
-
`INSERT INTO "${this.tableName}" (${dbCols.join(', ')}) VALUES (${placeholders.join(', ')}) ON CONFLICT (${conflictCols.map((c) => `"${c}"`).join(', ')}) DO UPDATE SET ${updateSet} RETURNING *`,
|
|
434
|
-
Object.values(filtered) as any[],
|
|
435
|
-
)
|
|
436
|
-
return row as unknown as R
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async count(sql: SqlClient, where?: Partial<R> | SQL | SQL[]): Promise<number> {
|
|
440
|
-
const { conditions, values } = this._buildConditions(where, 0)
|
|
441
|
-
|
|
442
|
-
const softDel = this._softDeleteFilter(where)
|
|
443
|
-
if (softDel) conditions.push(softDel)
|
|
444
|
-
|
|
445
|
-
const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(' AND ')}` : ''
|
|
446
|
-
const [row] = await sql.unsafe(
|
|
447
|
-
`SELECT COUNT(*) AS _total FROM "${this.tableName}"${whereClause}`,
|
|
448
|
-
values as any[],
|
|
449
|
-
)
|
|
450
|
-
return Number((row as Record<string, number>)._total)
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
export class BoundTable<R extends Record<string, unknown>> {
|
|
455
|
-
private inner: Table<R>
|
|
456
|
-
private sql: SqlClient
|
|
457
|
-
|
|
458
|
-
/** The underlying table name. */
|
|
459
|
-
get tableName(): string {
|
|
460
|
-
return this.inner.tableName
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
constructor(sql: SqlClient, tableName: string, builders: Record<string, ColumnBuilder<unknown>>) {
|
|
464
|
-
this.inner = new Table<R>(tableName, builders)
|
|
465
|
-
this.sql = sql
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
async create(opts?: CreateOptions): Promise<void> {
|
|
469
|
-
await this.inner.create(this.sql, opts)
|
|
470
|
-
}
|
|
471
|
-
async drop(opts?: { cascade?: boolean }): Promise<void> {
|
|
472
|
-
await this.inner.drop(this.sql, opts)
|
|
473
|
-
}
|
|
474
|
-
async createIndex(columns: string | string[], opts?: IndexOptions): Promise<void> {
|
|
475
|
-
await this.inner.createIndex(this.sql, columns, opts)
|
|
476
|
-
}
|
|
477
|
-
async createUniqueIndex(columns: string | string[]): Promise<void> {
|
|
478
|
-
await this.inner.createUniqueIndex(this.sql, columns)
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
async insert(data: Partial<R>): Promise<R> {
|
|
482
|
-
return await this.inner.insert(this.sql, data)
|
|
483
|
-
}
|
|
484
|
-
async insertMany(data: Partial<R>[]): Promise<R[]> {
|
|
485
|
-
return await this.inner.insertMany(this.sql, data)
|
|
486
|
-
}
|
|
487
|
-
async read(
|
|
488
|
-
id: string | number,
|
|
489
|
-
opts?: Pick<FindOptions, 'select' | 'withDeleted'>,
|
|
490
|
-
): Promise<R | undefined> {
|
|
491
|
-
return await this.inner.read(this.sql, id, opts)
|
|
492
|
-
}
|
|
493
|
-
async readMany(
|
|
494
|
-
where?: Partial<R> | SQL | SQL[],
|
|
495
|
-
opts?: FindOptions,
|
|
496
|
-
): Promise<{ count: number; data: R[] }> {
|
|
497
|
-
return await this.inner.readMany(this.sql, where, opts)
|
|
498
|
-
}
|
|
499
|
-
async update(id: string | number, data: Partial<R>): Promise<R | undefined> {
|
|
500
|
-
return await this.inner.update(this.sql, id, data)
|
|
501
|
-
}
|
|
502
|
-
async updateMany(where: Partial<R> | SQL | SQL[], data: Partial<R>): Promise<number> {
|
|
503
|
-
return await this.inner.updateMany(this.sql, where, data)
|
|
504
|
-
}
|
|
505
|
-
async delete(id: string | number): Promise<R | undefined> {
|
|
506
|
-
return await this.inner.delete(this.sql, id)
|
|
507
|
-
}
|
|
508
|
-
async hardDelete(id: string | number): Promise<R | undefined> {
|
|
509
|
-
return await this.inner.hardDelete(this.sql, id)
|
|
510
|
-
}
|
|
511
|
-
async deleteMany(where: Partial<R> | SQL | SQL[]): Promise<number> {
|
|
512
|
-
return await this.inner.deleteMany(this.sql, where)
|
|
513
|
-
}
|
|
514
|
-
async hardDeleteMany(where: Partial<R> | SQL | SQL[]): Promise<number> {
|
|
515
|
-
return await this.inner.hardDeleteMany(this.sql, where)
|
|
516
|
-
}
|
|
517
|
-
async upsert(data: Partial<R>, conflict: string | string[]): Promise<R> {
|
|
518
|
-
return await this.inner.upsert(this.sql, data, conflict)
|
|
519
|
-
}
|
|
520
|
-
async count(where?: Partial<R> | SQL | SQL[]): Promise<number> {
|
|
521
|
-
return await this.inner.count(this.sql, where)
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
withSql(sql: SqlClient): BoundTable<R> {
|
|
525
|
-
return new BoundTable(sql, this.inner.tableName, this.inner.builders)
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Define a type-safe table schema.
|
|
531
|
-
*
|
|
532
|
-
* ```ts
|
|
533
|
-
* const users = pgTable('users', {
|
|
534
|
-
* id: serial('id').primaryKey(),
|
|
535
|
-
* name: text('name').notNull(),
|
|
536
|
-
* email: text('email').unique(),
|
|
537
|
-
* })
|
|
538
|
-
*
|
|
539
|
-
* // The generic type R preserves the column types:
|
|
540
|
-
* // Table<{ id: number; name: string; email: string }>
|
|
541
|
-
* ```
|
|
542
|
-
*/
|
|
543
|
-
export function pgTable<R extends Record<string, unknown>>(
|
|
544
|
-
tableName: string,
|
|
545
|
-
builders: { [K in keyof R]: ColumnBuilder<R[K]> },
|
|
546
|
-
): Table<R> {
|
|
547
|
-
return new Table<R>(tableName, builders as unknown as Record<string, ColumnBuilder<unknown>>)
|
|
548
|
-
}
|
package/postgres/schema/where.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import { SQL } from './sql.ts'
|
|
3
|
-
|
|
4
|
-
function op(col: string, sqlOp: string, val: unknown): SQL {
|
|
5
|
-
return new SQL([`"${col}" ${sqlOp} `, ''] as any, [val])
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** Column equals value: `col = val`. */
|
|
9
|
-
export function eq(col: string, val: unknown): SQL {
|
|
10
|
-
return op(col, '=', val)
|
|
11
|
-
}
|
|
12
|
-
/** Column not equals value: `col != val`. */
|
|
13
|
-
export function ne(col: string, val: unknown): SQL {
|
|
14
|
-
return op(col, '!=', val)
|
|
15
|
-
}
|
|
16
|
-
/** Column greater than value: `col > val`. */
|
|
17
|
-
export function gt(col: string, val: unknown): SQL {
|
|
18
|
-
return op(col, '>', val)
|
|
19
|
-
}
|
|
20
|
-
/** Column greater than or equal value: `col >= val`. */
|
|
21
|
-
export function gte(col: string, val: unknown): SQL {
|
|
22
|
-
return op(col, '>=', val)
|
|
23
|
-
}
|
|
24
|
-
/** Column less than value: `col < val`. */
|
|
25
|
-
export function lt(col: string, val: unknown): SQL {
|
|
26
|
-
return op(col, '<', val)
|
|
27
|
-
}
|
|
28
|
-
/** Column less than or equal value: `col <= val`. */
|
|
29
|
-
export function lte(col: string, val: unknown): SQL {
|
|
30
|
-
return op(col, '<=', val)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Column IS NULL. */
|
|
34
|
-
export function isNull(col: string): SQL {
|
|
35
|
-
return new SQL([`"${col}" IS NULL`] as any, [])
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Column IS NOT NULL. */
|
|
39
|
-
export function isNotNull(col: string): SQL {
|
|
40
|
-
return new SQL([`"${col}" IS NOT NULL`] as any, [])
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Column LIKE pattern. */
|
|
44
|
-
export function like(col: string, pattern: string): SQL {
|
|
45
|
-
return op(col, 'LIKE', pattern)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Negate a condition: `NOT (condition)`. */
|
|
49
|
-
export function not(condition: SQL): SQL {
|
|
50
|
-
const strings = condition.strings
|
|
51
|
-
const values = condition.values
|
|
52
|
-
if (strings.length === 1 && strings[0] === '') return new SQL(['NOT ()'] as any, [])
|
|
53
|
-
const result: string[] = ['NOT (']
|
|
54
|
-
for (let i = 0; i < strings.length; i++) {
|
|
55
|
-
if (i > 0) result.push(strings[i])
|
|
56
|
-
else result[0] += strings[i]
|
|
57
|
-
}
|
|
58
|
-
result[result.length - 1] += ')'
|
|
59
|
-
return new SQL(result as any, [...values])
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** JSONB containment: `col @> val` (does `col` contain `val`?). */
|
|
63
|
-
export function contains(col: string, val: Record<string, unknown>): SQL {
|
|
64
|
-
return new SQL([`"${col}" @> `, ''] as any, [val])
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Column value is in array: `col = ANY(val)`. */
|
|
68
|
-
export function in_(col: string, val: unknown[]): SQL {
|
|
69
|
-
const close = ')'
|
|
70
|
-
return new SQL([`"${col}" = ANY(`, close] as any, [val])
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function combine(conditions: SQL[], joiner: string): SQL {
|
|
74
|
-
if (conditions.length === 0) return new SQL([''] as any, [])
|
|
75
|
-
const strings: string[] = ['(']
|
|
76
|
-
const values: unknown[] = []
|
|
77
|
-
for (let i = 0; i < conditions.length; i++) {
|
|
78
|
-
if (i > 0) strings[strings.length - 1] += ` ${joiner} `
|
|
79
|
-
const s = conditions[i]
|
|
80
|
-
for (let j = 0; j < s.strings.length; j++) {
|
|
81
|
-
strings[strings.length - 1] += s.strings[j]
|
|
82
|
-
if (j < s.values.length) {
|
|
83
|
-
strings.push('')
|
|
84
|
-
values.push(s.values[j])
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
strings[strings.length - 1] += ')'
|
|
89
|
-
return new SQL(strings as any, values)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Combine conditions with AND. */
|
|
93
|
-
export function and(...conditions: SQL[]): SQL {
|
|
94
|
-
return combine(conditions, 'AND')
|
|
95
|
-
}
|
|
96
|
-
/** Combine conditions with OR. */
|
|
97
|
-
export function or(...conditions: SQL[]): SQL {
|
|
98
|
-
return combine(conditions, 'OR')
|
|
99
|
-
}
|
package/postgres/types.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import type { SqlClient, Context, Middleware, Closeable } from '../types.ts'
|
|
3
|
-
import type { ColumnBuilder, BoundTable, Table } from './schema/index.ts'
|
|
4
|
-
|
|
5
|
-
declare module '../types.ts' {
|
|
6
|
-
interface Context {
|
|
7
|
-
sql: SqlClient
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface PostgresInjected {
|
|
12
|
-
sql: SqlClient
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface PostgresOptions {
|
|
16
|
-
connection?: string | Record<string, unknown>
|
|
17
|
-
signal?: AbortSignal
|
|
18
|
-
closeTimeout?: number
|
|
19
|
-
max?: number
|
|
20
|
-
ssl?: boolean | Record<string, unknown>
|
|
21
|
-
idle_timeout?: number
|
|
22
|
-
connect_timeout?: number
|
|
23
|
-
/** Per-statement timeout in ms. Set to 0 to disable. Default: 30_000. */
|
|
24
|
-
statementTimeout?: number
|
|
25
|
-
/** Called after every query completes. Receives query text, duration in ms, and row count. */
|
|
26
|
-
onQuery?: (query: string, durationMs: number, rowCount: number) => void
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface PostgresClient extends Middleware<Context, Context & PostgresInjected>, Closeable {
|
|
30
|
-
sql: SqlClient
|
|
31
|
-
/** Creates the migration tracking table (_weifuwu_migrations). Called once at startup. */
|
|
32
|
-
migrate: () => Promise<void>
|
|
33
|
-
/** Record that a module's migration has been applied (idempotent). */
|
|
34
|
-
markMigrated: (moduleName: string) => Promise<void>
|
|
35
|
-
/** Check whether a module has already been migrated. */
|
|
36
|
-
isMigrated: (moduleName: string) => Promise<boolean>
|
|
37
|
-
table: {
|
|
38
|
-
<R extends Record<string, unknown>>(
|
|
39
|
-
tableName: string,
|
|
40
|
-
builders: { [K in keyof R]: ColumnBuilder<R[K]> },
|
|
41
|
-
): BoundTable<R>
|
|
42
|
-
<R extends Record<string, unknown>>(schema: Table<R>): BoundTable<R>
|
|
43
|
-
}
|
|
44
|
-
transaction: <T>(fn: (sql: any) => Promise<T>, retryOpts?: { maxRetries?: number }) => Promise<T>
|
|
45
|
-
/** Snapshot of connection pool state: active, idle, waiting, max connections. */
|
|
46
|
-
poolStats: () => { active: number; idle: number; waiting: number; max: number }
|
|
47
|
-
close: () => Promise<void>
|
|
48
|
-
}
|