@hazejs/orm 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +27 -2
- package/dist/index.d.mts +125 -0
- package/dist/index.mjs +1067 -0
- package/dist/model-CBky_ILK.d.mts +728 -0
- package/dist/mongo/index.d.mts +21 -0
- package/dist/mongo/index.mjs +630 -0
- package/dist/record-DtkUCWRf.mjs +335 -0
- package/package.json +86 -3
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
import { _ as DatabaseError, a as Relation, b as translateDatabaseError, c as affectedRows, d as pascalCase, f as primaryKeyOf, g as useDatabaseManager, h as transaction, i as HasOne, l as isPlainObject, m as setDatabaseManager, n as BelongsTo, o as defaultKey, p as resolveSession, r as HasMany, s as loadRelations, t as ActiveRecord, u as nowFor, v as ModelNotFoundException, y as UniqueConstraintException } from "./record-DtkUCWRf.mjs";
|
|
2
|
+
import { Hash, Logger, definePersistent, usePlatformBinding } from "@hazejs/core";
|
|
3
|
+
import { mkdir, readFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { SQL, and, asc, avg, between, count, desc, eq, getTableColumns, getTableName, gt, gte, inArray, is, isNotNull, isNull, like, lt, lte, max, min, ne, notBetween, notInArray, notLike, or, sql, sum } from "drizzle-orm";
|
|
7
|
+
// src/auth.ts
|
|
8
|
+
/**
|
|
9
|
+
* 基于模型的用户来源(config/auth.ts 的 providers):
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* providers: { users: modelUserProvider(() => User, { username: 'email' }) }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* 模型以函数传入,避免配置文件与模型之间的循环导入。密码字段使用 `Hash.make()` 生成的哈希。
|
|
16
|
+
*/
|
|
17
|
+
function modelUserProvider(model, options = {}) {
|
|
18
|
+
const usernames = Array.isArray(options.username) ? options.username : [options.username ?? "email"];
|
|
19
|
+
const password = options.password ?? "password";
|
|
20
|
+
const statics = () => model();
|
|
21
|
+
return {
|
|
22
|
+
retrieveById: (id) => statics().find(id),
|
|
23
|
+
async retrieveByCredentials(credentials) {
|
|
24
|
+
const where = Object.fromEntries(usernames.filter((field) => credentials[field] !== void 0).map((field) => [field, credentials[field]]));
|
|
25
|
+
if (!Object.keys(where).length) return void 0;
|
|
26
|
+
return statics().where(where).find();
|
|
27
|
+
},
|
|
28
|
+
async validateCredentials(user, credentials) {
|
|
29
|
+
const hash = user[password];
|
|
30
|
+
const plain = credentials[password];
|
|
31
|
+
return typeof hash === "string" && typeof plain === "string" && Hash.verify(hash, plain);
|
|
32
|
+
},
|
|
33
|
+
getId: (user) => user.getKey()
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// src/config.ts
|
|
37
|
+
/** `config/database.ts` 的默认导出 */
|
|
38
|
+
function defineDatabaseConfig(config) {
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
function dialectOf(driver) {
|
|
42
|
+
switch (driver) {
|
|
43
|
+
case "pglite": return "postgres";
|
|
44
|
+
case "turso":
|
|
45
|
+
case "d1": return "sqlite";
|
|
46
|
+
default: return driver;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// src/connection.ts
|
|
50
|
+
/**
|
|
51
|
+
* 连接跨 HMR 重载保留(同名且配置不变时复用),只在应用真正关闭时释放
|
|
52
|
+
*/
|
|
53
|
+
function connections() {
|
|
54
|
+
return definePersistent("orm:connections", () => /* @__PURE__ */ new Map(), { dispose: async (entries) => {
|
|
55
|
+
await Promise.all([...entries.values()].map(async (entry) => (await entry.connection.catch(() => void 0))?.close()));
|
|
56
|
+
entries.clear();
|
|
57
|
+
} });
|
|
58
|
+
}
|
|
59
|
+
var DatabaseManager = class {
|
|
60
|
+
config;
|
|
61
|
+
#options;
|
|
62
|
+
constructor(config, options = {}) {
|
|
63
|
+
if (!config?.connections || !config.default) throw new DatabaseError("Invalid database config: `default` and `connections` are required (config/database.ts)");
|
|
64
|
+
this.config = config;
|
|
65
|
+
this.#options = options;
|
|
66
|
+
}
|
|
67
|
+
get defaultConnection() {
|
|
68
|
+
return this.config.default;
|
|
69
|
+
}
|
|
70
|
+
connectionConfig(name = this.config.default) {
|
|
71
|
+
const config = this.config.connections[name];
|
|
72
|
+
if (!config) throw new DatabaseError(`Database connection "${name}" is not defined in config/database.ts`);
|
|
73
|
+
return config;
|
|
74
|
+
}
|
|
75
|
+
/** 获取连接(首次使用时建立) */
|
|
76
|
+
connection(name = this.config.default) {
|
|
77
|
+
const config = this.connectionConfig(name);
|
|
78
|
+
const hash = JSON.stringify(config);
|
|
79
|
+
const pool = connections();
|
|
80
|
+
const existing = pool.get(name);
|
|
81
|
+
if (existing?.hash === hash) return existing.connection;
|
|
82
|
+
existing?.connection.then((connection) => connection.close(), () => void 0);
|
|
83
|
+
const connection = openConnection(name, config, this.#options);
|
|
84
|
+
pool.set(name, {
|
|
85
|
+
hash,
|
|
86
|
+
connection
|
|
87
|
+
});
|
|
88
|
+
connection.catch(() => pool.get(name)?.connection === connection && pool.delete(name));
|
|
89
|
+
return connection;
|
|
90
|
+
}
|
|
91
|
+
/** 关闭连接(默认全部);测试中可用于重建内存数据库 */
|
|
92
|
+
async close(name) {
|
|
93
|
+
const pool = connections();
|
|
94
|
+
const names = name ? [name] : [...pool.keys()];
|
|
95
|
+
for (const key of names) {
|
|
96
|
+
const entry = pool.get(key);
|
|
97
|
+
pool.delete(key);
|
|
98
|
+
await (await entry?.connection.catch(() => void 0))?.close();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
async function openConnection(name, config, options) {
|
|
103
|
+
const logger = config.logging && options.logger ? { logQuery: (query, params) => options.logger.debug(`[db:${name}] ${query}`, params.length ? params : "") } : void 0;
|
|
104
|
+
const base = {
|
|
105
|
+
name,
|
|
106
|
+
driver: config.driver,
|
|
107
|
+
dialect: dialectOf(config.driver)
|
|
108
|
+
};
|
|
109
|
+
switch (config.driver) {
|
|
110
|
+
case "sqlite": {
|
|
111
|
+
const { createClient } = await importDriver("@libsql/client", () => import("@libsql/client"));
|
|
112
|
+
const { drizzle } = await import("drizzle-orm/libsql");
|
|
113
|
+
const client = createClient({
|
|
114
|
+
url: await sqliteUrl(config.url ?? "file:./data/app.db", options.rootDir),
|
|
115
|
+
authToken: config.authToken
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
...base,
|
|
119
|
+
db: drizzle(client, { logger }),
|
|
120
|
+
close: async () => client.close()
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case "turso": {
|
|
124
|
+
const { createClient } = await importDriver("@libsql/client", () => import("@libsql/client/web"));
|
|
125
|
+
const { drizzle } = await import("drizzle-orm/libsql/web");
|
|
126
|
+
if (!config.url) throw new DatabaseError(`Connection "${name}": url is required for turso (libsql:// or https://)`);
|
|
127
|
+
const client = createClient({
|
|
128
|
+
url: config.url,
|
|
129
|
+
authToken: config.authToken
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
...base,
|
|
133
|
+
db: drizzle(client, { logger }),
|
|
134
|
+
close: async () => client.close()
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
case "d1": {
|
|
138
|
+
const { drizzle } = await import("drizzle-orm/d1");
|
|
139
|
+
const binding = typeof config.binding === "object" ? config.binding : usePlatformBinding(config.binding ?? "DB");
|
|
140
|
+
if (!binding) throw new DatabaseError(`Connection "${name}": D1 binding "${config.binding ?? "DB"}" not found. D1 is only available on Cloudflare Workers (preset cloudflare); use the sqlite driver locally`);
|
|
141
|
+
return {
|
|
142
|
+
...base,
|
|
143
|
+
db: drizzle(binding, { logger }),
|
|
144
|
+
close: async () => {}
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
case "postgres": {
|
|
148
|
+
const { default: postgres } = await importDriver("postgres", () => import("postgres"));
|
|
149
|
+
const { drizzle } = await import("drizzle-orm/postgres-js");
|
|
150
|
+
if (!config.url) throw new DatabaseError(`Connection "${name}": url is required for postgres`);
|
|
151
|
+
const client = postgres(config.url, {
|
|
152
|
+
max: config.pool ?? 10,
|
|
153
|
+
onnotice: () => {}
|
|
154
|
+
});
|
|
155
|
+
return {
|
|
156
|
+
...base,
|
|
157
|
+
db: drizzle(client, { logger }),
|
|
158
|
+
close: () => client.end()
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
case "pglite": {
|
|
162
|
+
const { PGlite } = await importDriver("@electric-sql/pglite", () => import("@electric-sql/pglite"));
|
|
163
|
+
const { drizzle } = await import("drizzle-orm/pglite");
|
|
164
|
+
const client = new PGlite(config.url);
|
|
165
|
+
return {
|
|
166
|
+
...base,
|
|
167
|
+
db: drizzle(client, { logger }),
|
|
168
|
+
close: () => client.close()
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
case "mysql": {
|
|
172
|
+
const mysql = await importDriver("mysql2", () => import("mysql2/promise"));
|
|
173
|
+
const { drizzle } = await import("drizzle-orm/mysql2");
|
|
174
|
+
if (!config.url) throw new DatabaseError(`Connection "${name}": url is required for mysql`);
|
|
175
|
+
const client = mysql.createPool({
|
|
176
|
+
uri: config.url,
|
|
177
|
+
connectionLimit: config.pool ?? 10
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
db: drizzle(client, {
|
|
182
|
+
logger,
|
|
183
|
+
mode: "default"
|
|
184
|
+
}),
|
|
185
|
+
close: () => client.end()
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
case "mongodb": {
|
|
189
|
+
const { MongoClient } = await importDriver("mongodb", () => import("mongodb"));
|
|
190
|
+
if (!config.url) throw new DatabaseError(`Connection "${name}": url is required for mongodb`);
|
|
191
|
+
const client = new MongoClient(config.url, { maxPoolSize: config.pool ?? 10 });
|
|
192
|
+
await client.connect();
|
|
193
|
+
return {
|
|
194
|
+
...base,
|
|
195
|
+
db: client.db(config.database),
|
|
196
|
+
client,
|
|
197
|
+
close: () => client.close()
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
default: throw new DatabaseError(`Connection "${name}": unknown driver "${config.driver}"`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* 加载驱动。导入路径须为字面量:打包器(Edge / 全量打包)据此静态解析驱动,
|
|
205
|
+
* 未安装或该平台不支持的驱动在构建时替换为调用即报错的空模块。
|
|
206
|
+
*/
|
|
207
|
+
async function importDriver(pkg, load) {
|
|
208
|
+
try {
|
|
209
|
+
return await load();
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new DatabaseError(`Database driver "${pkg}" could not be loaded. Install it with: pnpm add ${pkg}`, { cause: error });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** file: 相对路径按项目根解析,并确保目录存在 */
|
|
215
|
+
async function sqliteUrl(url, rootDir = process.cwd()) {
|
|
216
|
+
if (!url.startsWith("file:")) return url;
|
|
217
|
+
const path = url.slice(5);
|
|
218
|
+
const absolute = isAbsolute(path) ? path : resolve(rootDir, path);
|
|
219
|
+
await mkdir(dirname(absolute), { recursive: true });
|
|
220
|
+
return `file:${absolute}`;
|
|
221
|
+
}
|
|
222
|
+
// src/query/conditions.ts
|
|
223
|
+
/** 条件构造:多次 where 以 AND 连接,orWhere 以 OR 连接(从左到右),回调参数用于分组 */
|
|
224
|
+
var Conditions = class Conditions {
|
|
225
|
+
table;
|
|
226
|
+
parts = [];
|
|
227
|
+
constructor(table) {
|
|
228
|
+
this.table = table;
|
|
229
|
+
}
|
|
230
|
+
where(...args) {
|
|
231
|
+
return this.addCondition("and", args);
|
|
232
|
+
}
|
|
233
|
+
orWhere(...args) {
|
|
234
|
+
return this.addCondition("or", args);
|
|
235
|
+
}
|
|
236
|
+
whereIn(column, values) {
|
|
237
|
+
return this.pushCondition("and", inArray(this.columnOf(column), values));
|
|
238
|
+
}
|
|
239
|
+
whereNotIn(column, values) {
|
|
240
|
+
return this.pushCondition("and", notInArray(this.columnOf(column), values));
|
|
241
|
+
}
|
|
242
|
+
whereNull(column) {
|
|
243
|
+
return this.pushCondition("and", isNull(this.columnOf(column)));
|
|
244
|
+
}
|
|
245
|
+
whereNotNull(column) {
|
|
246
|
+
return this.pushCondition("and", isNotNull(this.columnOf(column)));
|
|
247
|
+
}
|
|
248
|
+
whereBetween(column, range) {
|
|
249
|
+
return this.pushCondition("and", between(this.columnOf(column), range[0], range[1]));
|
|
250
|
+
}
|
|
251
|
+
whereNotBetween(column, range) {
|
|
252
|
+
return this.pushCondition("and", notBetween(this.columnOf(column), range[0], range[1]));
|
|
253
|
+
}
|
|
254
|
+
whereLike(column, pattern) {
|
|
255
|
+
return this.pushCondition("and", like(this.columnOf(column), pattern));
|
|
256
|
+
}
|
|
257
|
+
whereNotLike(column, pattern) {
|
|
258
|
+
return this.pushCondition("and", notLike(this.columnOf(column), pattern));
|
|
259
|
+
}
|
|
260
|
+
/** 条件成立时才应用:`.when(keyword, q => q.whereLike('name', `%${keyword}%`))` */
|
|
261
|
+
when(value, callback) {
|
|
262
|
+
if (value !== void 0 && value !== null && value !== false && value !== "") callback(this, value);
|
|
263
|
+
return this;
|
|
264
|
+
}
|
|
265
|
+
/** 当前条件组合成的 SQL(无条件时为 undefined) */
|
|
266
|
+
buildWhere() {
|
|
267
|
+
let result;
|
|
268
|
+
for (const part of this.parts) result = result === void 0 ? part.sql : part.bool === "and" ? and(result, part.sql) : or(result, part.sql);
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
get hasConditions() {
|
|
272
|
+
return this.parts.length > 0;
|
|
273
|
+
}
|
|
274
|
+
/** 列名 → drizzle 列对象 */
|
|
275
|
+
columnOf(key) {
|
|
276
|
+
const column = getTableColumns(this.table)[key];
|
|
277
|
+
if (!column) throw new Error(`Unknown column "${key}" on table "${getTableName(this.table)}"`);
|
|
278
|
+
return column;
|
|
279
|
+
}
|
|
280
|
+
addCondition(bool, args) {
|
|
281
|
+
const condition = this.parseCondition(args);
|
|
282
|
+
return condition ? this.pushCondition(bool, condition) : this;
|
|
283
|
+
}
|
|
284
|
+
pushCondition(bool, sql) {
|
|
285
|
+
this.parts.push({
|
|
286
|
+
bool,
|
|
287
|
+
sql
|
|
288
|
+
});
|
|
289
|
+
return this;
|
|
290
|
+
}
|
|
291
|
+
parseCondition(args) {
|
|
292
|
+
if (args.length === 1) {
|
|
293
|
+
const [condition] = args;
|
|
294
|
+
if (typeof condition === "function") {
|
|
295
|
+
const group = new Conditions(this.table);
|
|
296
|
+
condition(group);
|
|
297
|
+
return group.buildWhere();
|
|
298
|
+
}
|
|
299
|
+
if (is(condition, SQL)) return condition;
|
|
300
|
+
if (isPlainObject(condition)) return and(...Object.entries(condition).map(([key, value]) => this.compare(key, "=", value)));
|
|
301
|
+
throw new TypeError("where() expects a column, an object, a drizzle SQL expression or a callback");
|
|
302
|
+
}
|
|
303
|
+
if (args.length === 2) return this.compare(args[0], "=", args[1]);
|
|
304
|
+
return this.compare(args[0], args[1], args[2]);
|
|
305
|
+
}
|
|
306
|
+
compare(key, operator, value) {
|
|
307
|
+
const column = this.columnOf(key);
|
|
308
|
+
switch (operator) {
|
|
309
|
+
case "=":
|
|
310
|
+
if (value === null) return isNull(column);
|
|
311
|
+
return Array.isArray(value) ? inArray(column, value) : eq(column, value);
|
|
312
|
+
case "!=":
|
|
313
|
+
case "<>":
|
|
314
|
+
if (value === null) return isNotNull(column);
|
|
315
|
+
return Array.isArray(value) ? notInArray(column, value) : ne(column, value);
|
|
316
|
+
case ">": return gt(column, value);
|
|
317
|
+
case ">=": return gte(column, value);
|
|
318
|
+
case "<": return lt(column, value);
|
|
319
|
+
case "<=": return lte(column, value);
|
|
320
|
+
case "like": return like(column, value);
|
|
321
|
+
case "not like": return notLike(column, value);
|
|
322
|
+
case "in": return inArray(column, value);
|
|
323
|
+
case "not in": return notInArray(column, value);
|
|
324
|
+
case "between": return between(column, value[0], value[1]);
|
|
325
|
+
case "not between": return notBetween(column, value[0], value[1]);
|
|
326
|
+
default: throw new Error(`Unsupported operator "${String(operator)}"`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
// src/query/builder.ts
|
|
331
|
+
/**
|
|
332
|
+
* ThinkPHP 风格的查询构造器,底层使用 drizzle:
|
|
333
|
+
*
|
|
334
|
+
* ```ts
|
|
335
|
+
* await Db.table(users).where('status', 1).order('id', 'desc').paginate(1, 20)
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
var QueryBuilder = class extends Conditions {
|
|
339
|
+
connectionName;
|
|
340
|
+
orders = [];
|
|
341
|
+
limitValue;
|
|
342
|
+
offsetValue;
|
|
343
|
+
fields;
|
|
344
|
+
constructor(table, options = {}) {
|
|
345
|
+
super(table);
|
|
346
|
+
this.connectionName = options.connection;
|
|
347
|
+
}
|
|
348
|
+
/** 切换连接 */
|
|
349
|
+
connection(name) {
|
|
350
|
+
this.connectionName = name;
|
|
351
|
+
return this;
|
|
352
|
+
}
|
|
353
|
+
order(column, direction = "asc") {
|
|
354
|
+
if (is(column, SQL)) this.orders.push(column);
|
|
355
|
+
else if (typeof column === "string") this.orders.push(direction === "desc" ? desc(this.columnOf(column)) : asc(this.columnOf(column)));
|
|
356
|
+
else for (const [key, dir] of Object.entries(column)) this.order(key, dir ?? "asc");
|
|
357
|
+
return this;
|
|
358
|
+
}
|
|
359
|
+
limit(value) {
|
|
360
|
+
this.limitValue = value;
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
offset(value) {
|
|
364
|
+
this.offsetValue = value;
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
/** 分页:page 从 1 开始 */
|
|
368
|
+
page(page, perPage = 15) {
|
|
369
|
+
return this.limit(perPage).offset((Math.max(1, page) - 1) * perPage);
|
|
370
|
+
}
|
|
371
|
+
/** 只查询指定列 */
|
|
372
|
+
field(...keys) {
|
|
373
|
+
this.fields = keys;
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
/** 查询全部结果 */
|
|
377
|
+
async select() {
|
|
378
|
+
const session = await this.session();
|
|
379
|
+
const selection = this.fields ? Object.fromEntries(this.fields.map((key) => [key, this.columnOf(key)])) : void 0;
|
|
380
|
+
let query = (selection ? session.db.select(selection) : session.db.select()).from(this.table).$dynamic();
|
|
381
|
+
const where = this.scopedWhere();
|
|
382
|
+
if (where) query = query.where(where);
|
|
383
|
+
if (this.orders.length) query = query.orderBy(...this.orders);
|
|
384
|
+
if (this.limitValue !== void 0) query = query.limit(this.limitValue);
|
|
385
|
+
if (this.offsetValue !== void 0) query = query.offset(this.offsetValue);
|
|
386
|
+
return this.transform(await query);
|
|
387
|
+
}
|
|
388
|
+
/** 查询第一条,不存在时为 undefined */
|
|
389
|
+
async find() {
|
|
390
|
+
const [first] = await this.clone().limit(1).select();
|
|
391
|
+
return first;
|
|
392
|
+
}
|
|
393
|
+
async findOrFail() {
|
|
394
|
+
const result = await this.find();
|
|
395
|
+
if (result === void 0) throw new ModelNotFoundException(this.label());
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
/** 某一列的值(第一条) */
|
|
399
|
+
async value(column) {
|
|
400
|
+
return (await this.clone().field(column).find())?.[column];
|
|
401
|
+
}
|
|
402
|
+
/** 某一列的全部值 */
|
|
403
|
+
async column(column) {
|
|
404
|
+
return (await this.clone().field(column).select()).map((row) => row[column]);
|
|
405
|
+
}
|
|
406
|
+
async count() {
|
|
407
|
+
return Number(await this.aggregate(count()) ?? 0);
|
|
408
|
+
}
|
|
409
|
+
async sum(column) {
|
|
410
|
+
return Number(await this.aggregate(sum(this.columnOf(column))) ?? 0);
|
|
411
|
+
}
|
|
412
|
+
async avg(column) {
|
|
413
|
+
const value = await this.aggregate(avg(this.columnOf(column)));
|
|
414
|
+
return value === null || value === void 0 ? null : Number(value);
|
|
415
|
+
}
|
|
416
|
+
async max(column) {
|
|
417
|
+
return await this.aggregate(max(this.columnOf(column)));
|
|
418
|
+
}
|
|
419
|
+
async min(column) {
|
|
420
|
+
return await this.aggregate(min(this.columnOf(column)));
|
|
421
|
+
}
|
|
422
|
+
async exists() {
|
|
423
|
+
return await this.count() > 0;
|
|
424
|
+
}
|
|
425
|
+
async paginate(page = 1, perPage = 15) {
|
|
426
|
+
const current = Math.max(1, Math.floor(page));
|
|
427
|
+
const total = await this.count();
|
|
428
|
+
return {
|
|
429
|
+
data: await this.clone().page(current, perPage).select(),
|
|
430
|
+
total,
|
|
431
|
+
page: current,
|
|
432
|
+
perPage,
|
|
433
|
+
lastPage: Math.max(1, Math.ceil(total / perPage))
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/** 分批处理大量数据,回调返回 false 时停止 */
|
|
437
|
+
async chunk(size, callback) {
|
|
438
|
+
for (let page = 1;; page++) {
|
|
439
|
+
const rows = await this.clone().page(page, size).select();
|
|
440
|
+
if (!rows.length || await callback(rows, page) === false || rows.length < size) return;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** 插入一条或多条,返回插入后的完整记录(MySQL 通过主键回查) */
|
|
444
|
+
async insert(values) {
|
|
445
|
+
const list = Array.isArray(values) ? values : [values];
|
|
446
|
+
if (!list.length) return [];
|
|
447
|
+
const session = await this.session();
|
|
448
|
+
const query = session.db.insert(this.table).values(list);
|
|
449
|
+
if (session.dialect !== "mysql") return this.write(() => query.returning());
|
|
450
|
+
const key = primaryKeyOf(this.table);
|
|
451
|
+
const ids = await this.write(() => query.$returningId());
|
|
452
|
+
if (!key || !ids.length) return [];
|
|
453
|
+
return session.db.select().from(this.table).where(inArray(this.columnOf(key), ids.map((id) => id[key])));
|
|
454
|
+
}
|
|
455
|
+
/** 插入一条并返回主键值 */
|
|
456
|
+
async insertGetId(values) {
|
|
457
|
+
const key = primaryKeyOf(this.table);
|
|
458
|
+
if (!key) throw new DatabaseError(`Table "${getTableName(this.table)}" has no single-column primary key`);
|
|
459
|
+
const [row] = await this.insert(values);
|
|
460
|
+
return row?.[key];
|
|
461
|
+
}
|
|
462
|
+
/** 按当前条件更新,返回影响行数 */
|
|
463
|
+
async update(values, options = {}) {
|
|
464
|
+
const where = this.writableWhere("update", options);
|
|
465
|
+
const session = await this.session();
|
|
466
|
+
return affectedRows(await this.write(() => session.db.update(this.table).set(values).where(where)));
|
|
467
|
+
}
|
|
468
|
+
/** 按当前条件删除,返回影响行数 */
|
|
469
|
+
async delete(options = {}) {
|
|
470
|
+
const where = this.writableWhere("delete", options);
|
|
471
|
+
const session = await this.session();
|
|
472
|
+
return affectedRows(await this.write(() => session.db.delete(this.table).where(where)));
|
|
473
|
+
}
|
|
474
|
+
async increment(column, step = 1) {
|
|
475
|
+
const target = this.columnOf(column);
|
|
476
|
+
return this.update({ [column]: sql`${target} + ${step}` });
|
|
477
|
+
}
|
|
478
|
+
async decrement(column, step = 1) {
|
|
479
|
+
const target = this.columnOf(column);
|
|
480
|
+
return this.update({ [column]: sql`${target} - ${step}` });
|
|
481
|
+
}
|
|
482
|
+
/** 生成的 SQL 与参数(调试用) */
|
|
483
|
+
async toSQL() {
|
|
484
|
+
let query = (await this.session()).db.select().from(this.table).$dynamic();
|
|
485
|
+
const where = this.scopedWhere();
|
|
486
|
+
if (where) query = query.where(where);
|
|
487
|
+
if (this.orders.length) query = query.orderBy(...this.orders);
|
|
488
|
+
if (this.limitValue !== void 0) query = query.limit(this.limitValue);
|
|
489
|
+
if (this.offsetValue !== void 0) query = query.offset(this.offsetValue);
|
|
490
|
+
return query.toSQL();
|
|
491
|
+
}
|
|
492
|
+
clone() {
|
|
493
|
+
const copy = Object.create(Object.getPrototypeOf(this));
|
|
494
|
+
Object.assign(copy, this);
|
|
495
|
+
copy.parts = [...this.parts];
|
|
496
|
+
copy.orders = [...this.orders];
|
|
497
|
+
copy.fields = this.fields && [...this.fields];
|
|
498
|
+
return copy;
|
|
499
|
+
}
|
|
500
|
+
/** 子类(ModelQuery)追加的全局条件,例如软删除 */
|
|
501
|
+
scopedWhere() {
|
|
502
|
+
return this.buildWhere();
|
|
503
|
+
}
|
|
504
|
+
/** 子类(ModelQuery)把行转换为模型 */
|
|
505
|
+
async transform(rows) {
|
|
506
|
+
return rows;
|
|
507
|
+
}
|
|
508
|
+
label() {
|
|
509
|
+
return getTableName(this.table);
|
|
510
|
+
}
|
|
511
|
+
session() {
|
|
512
|
+
return resolveSession(this.connectionName);
|
|
513
|
+
}
|
|
514
|
+
async aggregate(expression) {
|
|
515
|
+
let query = (await this.session()).db.select({ value: expression }).from(this.table).$dynamic();
|
|
516
|
+
const where = this.scopedWhere();
|
|
517
|
+
if (where) query = query.where(where);
|
|
518
|
+
const [row] = await query;
|
|
519
|
+
return row?.value;
|
|
520
|
+
}
|
|
521
|
+
/** 写操作:把唯一约束等数据库错误转换为框架异常(409) */
|
|
522
|
+
async write(run) {
|
|
523
|
+
try {
|
|
524
|
+
return await run();
|
|
525
|
+
} catch (error) {
|
|
526
|
+
throw translateDatabaseError(error);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
writableWhere(action, options) {
|
|
530
|
+
const where = this.scopedWhere();
|
|
531
|
+
if (!where && !options.all) throw new DatabaseError(`Refusing to ${action} all rows of "${getTableName(this.table)}" without conditions. Pass { all: true } to confirm.`);
|
|
532
|
+
return where;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
/** 将多个条件以 AND 合并(忽略 undefined) */
|
|
536
|
+
function andAll(...conditions) {
|
|
537
|
+
const defined = conditions.filter((condition) => condition !== void 0);
|
|
538
|
+
return defined.length > 1 ? and(...defined) : defined[0];
|
|
539
|
+
}
|
|
540
|
+
// src/db.ts
|
|
541
|
+
/** 执行原生 SQL,统一返回行数组 */
|
|
542
|
+
async function execute(query, connection) {
|
|
543
|
+
const { db, dialect } = await resolveSession(connection);
|
|
544
|
+
if (dialect === "sqlite") return db.all(query);
|
|
545
|
+
const result = await db.execute(query);
|
|
546
|
+
if (dialect === "mysql") return result[0];
|
|
547
|
+
return Array.isArray(result) ? result : result.rows;
|
|
548
|
+
}
|
|
549
|
+
function scoped(connection) {
|
|
550
|
+
return {
|
|
551
|
+
/** 查询构造器:`Db.table(users).where('id', 1).find()` */
|
|
552
|
+
table: (table) => new QueryBuilder(table, { connection }),
|
|
553
|
+
/** 事务(回调内的 Db / Model 查询自动加入事务,嵌套使用保存点) */
|
|
554
|
+
transaction: (callback) => transaction(callback, connection),
|
|
555
|
+
/** 原生 SQL:`Db.execute(sql\`select 1\`)` */
|
|
556
|
+
execute: (query) => execute(query, connection),
|
|
557
|
+
/** 底层 drizzle 实例(处于事务中时为事务对象),用于 join 等复杂查询 */
|
|
558
|
+
drizzle: async () => (await resolveSession(connection)).db
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
/** 数据库门面:默认连接的快捷入口,`Db.connection(name)` 切换连接 */
|
|
562
|
+
const Db = {
|
|
563
|
+
...scoped(),
|
|
564
|
+
connection: (name) => scoped(name)
|
|
565
|
+
};
|
|
566
|
+
// src/migrate.ts
|
|
567
|
+
/**
|
|
568
|
+
* 执行 drizzle-kit 生成的迁移(已执行过的会跳过)。迁移记录与 drizzle 自带的迁移器相同,
|
|
569
|
+
* 可与 drizzle-kit migrate 混用。
|
|
570
|
+
*/
|
|
571
|
+
async function migrate(options) {
|
|
572
|
+
const manager = options.manager ?? useDatabaseManager();
|
|
573
|
+
const migrations = await readMigrations(options.folder, options.files);
|
|
574
|
+
const connection = await manager.connection(options.connection);
|
|
575
|
+
if (connection.driver === "mongodb") throw new DatabaseError(`Connection "${connection.name}" is MongoDB, which has no SQL migrations; manage indexes with Mongo.collection().createIndex()`);
|
|
576
|
+
const before = await appliedCount(connection);
|
|
577
|
+
const { db } = connection;
|
|
578
|
+
switch (connection.driver) {
|
|
579
|
+
case "sqlite":
|
|
580
|
+
case "turso":
|
|
581
|
+
await migrateSqlite(db, migrations, (statements) => db.session.migrate(statements));
|
|
582
|
+
break;
|
|
583
|
+
case "d1":
|
|
584
|
+
await migrateSqlite(db, migrations, async (statements) => statements.length && db.session.batch(statements), true);
|
|
585
|
+
break;
|
|
586
|
+
default: await db.dialect.migrate(migrations, db.session, { migrationsFolder: options.folder });
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
connection: connection.name,
|
|
590
|
+
folder: options.folder,
|
|
591
|
+
applied: await appliedCount(connection) - before,
|
|
592
|
+
total: migrations.length
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
/** 迁移目录(相对后端源码目录):连接配置的 migrations,默认连接为 database/migrations,其它为 database/migrations/<name> */
|
|
596
|
+
function migrationsFolder(name, manager = useDatabaseManager()) {
|
|
597
|
+
const connection = name ?? manager.defaultConnection;
|
|
598
|
+
return (manager.connectionConfig(connection).migrations ?? (connection === manager.defaultConnection ? "database/migrations" : `database/migrations/${connection}`)).replace(/^\.\//, "").replace(/\/+$/, "");
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* 按 config/database.ts 执行迁移:`haze db:migrate` 与生产构建的迁移入口
|
|
602
|
+
* (`node dist/server/migrate.mjs`、Lambda 的 `index.migrate`)共用。
|
|
603
|
+
*/
|
|
604
|
+
async function runMigrations(options = {}) {
|
|
605
|
+
const manager = options.manager ?? useDatabaseManager();
|
|
606
|
+
const names = options.all ? Object.keys(manager.config.connections) : [options.connection ?? manager.defaultConnection];
|
|
607
|
+
const results = [];
|
|
608
|
+
for (const name of names) {
|
|
609
|
+
const relative = migrationsFolder(name, manager);
|
|
610
|
+
if (manager.connectionConfig(name).driver === "mongodb") {
|
|
611
|
+
results.push({
|
|
612
|
+
connection: name,
|
|
613
|
+
folder: relative,
|
|
614
|
+
skipped: "mongodb"
|
|
615
|
+
});
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const folder = options.files || isAbsolute(relative) ? relative : resolve(options.srcDir ?? process.cwd(), relative);
|
|
619
|
+
if (!await readFile$1(journalPath(folder), options.files)) {
|
|
620
|
+
if (!options.all) throw new DatabaseError(`No migrations for connection "${name}" in ${relative}. Run haze db:generate first.`);
|
|
621
|
+
results.push({
|
|
622
|
+
connection: name,
|
|
623
|
+
folder: relative,
|
|
624
|
+
skipped: "no-migrations"
|
|
625
|
+
});
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
results.push({
|
|
629
|
+
...await migrate({
|
|
630
|
+
connection: name,
|
|
631
|
+
folder,
|
|
632
|
+
files: options.files,
|
|
633
|
+
manager
|
|
634
|
+
}),
|
|
635
|
+
folder: relative
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return results;
|
|
639
|
+
}
|
|
640
|
+
/** 与 drizzle 的 readMigrationFiles 相同的解析(哈希为整个文件的 SHA-256),可读取内嵌文件 */
|
|
641
|
+
async function readMigrations(folder, files) {
|
|
642
|
+
const journal = await readFile$1(journalPath(folder), files);
|
|
643
|
+
if (journal === void 0) throw new DatabaseError(`Migrations not found in ${folder} (meta/_journal.json is missing). Run haze db:generate first.`);
|
|
644
|
+
const entries = [];
|
|
645
|
+
for (const entry of JSON.parse(journal).entries) {
|
|
646
|
+
const query = await readFile$1(`${trimSlash(folder)}/${entry.tag}.sql`, files);
|
|
647
|
+
if (query === void 0) throw new DatabaseError(`Migration file ${entry.tag}.sql not found in ${folder}`);
|
|
648
|
+
entries.push({
|
|
649
|
+
sql: query.split("--> statement-breakpoint"),
|
|
650
|
+
bps: entry.breakpoints,
|
|
651
|
+
folderMillis: entry.when,
|
|
652
|
+
hash: await sha256(query)
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
return entries;
|
|
656
|
+
}
|
|
657
|
+
/** 与 drizzle 的 libsql / d1 迁移器相同:一次批量执行全部未运行的迁移 */
|
|
658
|
+
async function migrateSqlite(db, migrations, execute, literal = false) {
|
|
659
|
+
const table = sql.identifier("__drizzle_migrations");
|
|
660
|
+
await db.session.run(sql`CREATE TABLE IF NOT EXISTS ${table} (id SERIAL PRIMARY KEY, hash text NOT NULL, created_at numeric)`);
|
|
661
|
+
const [last] = await db.values(sql`SELECT id, hash, created_at FROM ${table} ORDER BY created_at DESC LIMIT 1`);
|
|
662
|
+
const statements = [];
|
|
663
|
+
for (const migration of migrations) {
|
|
664
|
+
if (last && Number(last[2]) >= migration.folderMillis) continue;
|
|
665
|
+
for (const statement of migration.sql) statements.push(db.run(sql.raw(statement)));
|
|
666
|
+
statements.push(db.run(literal ? sql`INSERT INTO ${table} ("hash", "created_at") VALUES(${sql.raw(`'${migration.hash}'`)}, ${sql.raw(String(migration.folderMillis))})` : sql`INSERT INTO ${table} ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`));
|
|
667
|
+
}
|
|
668
|
+
await execute(statements);
|
|
669
|
+
}
|
|
670
|
+
/** 已执行的迁移数(迁移表不存在时为 0) */
|
|
671
|
+
async function appliedCount(connection) {
|
|
672
|
+
const table = connection.dialect === "postgres" ? sql`"drizzle"."__drizzle_migrations"` : sql`${sql.identifier("__drizzle_migrations")}`;
|
|
673
|
+
const query = sql`select count(*) as n from ${table}`;
|
|
674
|
+
const { db } = connection;
|
|
675
|
+
try {
|
|
676
|
+
const result = connection.dialect === "sqlite" ? await db.all(query) : await db.execute(query);
|
|
677
|
+
const rows = connection.dialect === "mysql" ? result[0] : Array.isArray(result) ? result : result.rows;
|
|
678
|
+
return Number(rows[0]?.n ?? 0);
|
|
679
|
+
} catch {
|
|
680
|
+
return 0;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
async function readFile$1(path, files) {
|
|
684
|
+
if (files) return files[path];
|
|
685
|
+
return readFile(path, "utf8").catch(() => void 0);
|
|
686
|
+
}
|
|
687
|
+
function journalPath(folder) {
|
|
688
|
+
return `${trimSlash(folder)}/meta/_journal.json`;
|
|
689
|
+
}
|
|
690
|
+
function trimSlash(path) {
|
|
691
|
+
return path.replace(/\/+$/, "");
|
|
692
|
+
}
|
|
693
|
+
async function sha256(text) {
|
|
694
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
695
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
696
|
+
}
|
|
697
|
+
// src/model/meta.ts
|
|
698
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
699
|
+
/** 解析并缓存模型元数据(主键、软删除列、时间戳列) */
|
|
700
|
+
function metaOf(model) {
|
|
701
|
+
const cached = cache.get(model);
|
|
702
|
+
if (cached) return cached;
|
|
703
|
+
if (!model.table) throw new DatabaseError(`Model ${model.name} has no table. Use: class ${model.name} extends Model.define(table) {}`);
|
|
704
|
+
const columns = getTableColumns(model.table);
|
|
705
|
+
const pick = (key, fallback, required) => {
|
|
706
|
+
if (key === false) return void 0;
|
|
707
|
+
const name = key ?? fallback;
|
|
708
|
+
if (name in columns) return name;
|
|
709
|
+
if (required || key) throw new DatabaseError(`Model ${model.name}: column "${name}" does not exist on table "${getTableName(model.table)}"`);
|
|
710
|
+
};
|
|
711
|
+
const timestamps = model.timestamps === false ? {
|
|
712
|
+
createdAt: false,
|
|
713
|
+
updatedAt: false
|
|
714
|
+
} : model.timestamps === true ? {} : model.timestamps;
|
|
715
|
+
const meta = {
|
|
716
|
+
table: model.table,
|
|
717
|
+
tableName: getTableName(model.table),
|
|
718
|
+
columns,
|
|
719
|
+
primaryKey: primaryKeyOf(model.table),
|
|
720
|
+
softDelete: model.softDelete ? pick(typeof model.softDelete === "string" ? model.softDelete : void 0, "deletedAt", true) : void 0,
|
|
721
|
+
createdAt: pick(timestamps.createdAt, "createdAt", false),
|
|
722
|
+
updatedAt: pick(timestamps.updatedAt, "updatedAt", false)
|
|
723
|
+
};
|
|
724
|
+
cache.set(model, meta);
|
|
725
|
+
return meta;
|
|
726
|
+
}
|
|
727
|
+
// src/model/query.ts
|
|
728
|
+
/** 模型查询:结果为模型实例,自动应用软删除范围,支持预加载关联与作用域 */
|
|
729
|
+
var ModelQuery = class extends QueryBuilder {
|
|
730
|
+
model;
|
|
731
|
+
trashed = "without";
|
|
732
|
+
eager = [];
|
|
733
|
+
constructor(model) {
|
|
734
|
+
super(metaOf(model).table, { connection: model.connection });
|
|
735
|
+
this.model = model;
|
|
736
|
+
}
|
|
737
|
+
/** 包含已软删除的数据 */
|
|
738
|
+
withTrashed() {
|
|
739
|
+
this.trashed = "with";
|
|
740
|
+
return this;
|
|
741
|
+
}
|
|
742
|
+
/** 只查询已软删除的数据 */
|
|
743
|
+
onlyTrashed() {
|
|
744
|
+
this.trashed = "only";
|
|
745
|
+
return this;
|
|
746
|
+
}
|
|
747
|
+
/** 预加载关联:`with('posts', 'posts.comments', 'profile')` */
|
|
748
|
+
with(...relations) {
|
|
749
|
+
this.eager.push(...relations);
|
|
750
|
+
return this;
|
|
751
|
+
}
|
|
752
|
+
/** 调用模型上的 `static scopeXxx(query, ...args)`:`scope('active')` */
|
|
753
|
+
scope(name, ...args) {
|
|
754
|
+
const method = this.model[`scope${pascalCase(name)}`];
|
|
755
|
+
if (typeof method !== "function") throw new TypeError(`Scope "${name}" is not defined on ${this.model.name} (expected static scope${pascalCase(name)}(query))`);
|
|
756
|
+
method.call(this.model, this, ...args);
|
|
757
|
+
return this;
|
|
758
|
+
}
|
|
759
|
+
/** 按主键查询;不传主键时取第一条 */
|
|
760
|
+
async find(id) {
|
|
761
|
+
if (id === void 0) return super.find();
|
|
762
|
+
return this.clone().where(this.primaryKey(), id).limit(1).select().then((rows) => rows[0]);
|
|
763
|
+
}
|
|
764
|
+
async findOrFail(id) {
|
|
765
|
+
const model = await this.find(id);
|
|
766
|
+
if (!model) throw new ModelNotFoundException(this.model.name, id);
|
|
767
|
+
return model;
|
|
768
|
+
}
|
|
769
|
+
paginate(page = 1, perPage = 15) {
|
|
770
|
+
return super.paginate(page, perPage);
|
|
771
|
+
}
|
|
772
|
+
/** 批量删除:启用软删除时只标记删除时间 */
|
|
773
|
+
async delete(options = {}) {
|
|
774
|
+
const { softDelete, columns } = metaOf(this.model);
|
|
775
|
+
if (softDelete && this.trashed !== "only") return this.update({ [softDelete]: nowFor(columns[softDelete]) }, options);
|
|
776
|
+
return super.delete(options);
|
|
777
|
+
}
|
|
778
|
+
/** 物理删除(忽略软删除) */
|
|
779
|
+
async forceDelete(options = {}) {
|
|
780
|
+
return super.delete(options);
|
|
781
|
+
}
|
|
782
|
+
/** 恢复软删除的数据 */
|
|
783
|
+
async restore(options = {}) {
|
|
784
|
+
const { softDelete } = metaOf(this.model);
|
|
785
|
+
if (!softDelete) return 0;
|
|
786
|
+
return this.withTrashed().update({ [softDelete]: null }, options);
|
|
787
|
+
}
|
|
788
|
+
scopedWhere() {
|
|
789
|
+
const { softDelete } = metaOf(this.model);
|
|
790
|
+
if (!softDelete || this.trashed === "with") return this.buildWhere();
|
|
791
|
+
const column = this.columnOf(softDelete);
|
|
792
|
+
return andAll(this.buildWhere(), this.trashed === "only" ? isNotNull(column) : isNull(column));
|
|
793
|
+
}
|
|
794
|
+
async transform(rows) {
|
|
795
|
+
const models = rows.map((row) => hydrateModel(this.model, row));
|
|
796
|
+
if (this.eager.length) await loadRelations(models, this.eager);
|
|
797
|
+
return models;
|
|
798
|
+
}
|
|
799
|
+
label() {
|
|
800
|
+
return this.model.name;
|
|
801
|
+
}
|
|
802
|
+
clone() {
|
|
803
|
+
const copy = super.clone();
|
|
804
|
+
copy.eager = [...this.eager];
|
|
805
|
+
return copy;
|
|
806
|
+
}
|
|
807
|
+
primaryKey() {
|
|
808
|
+
const key = metaOf(this.model).primaryKey;
|
|
809
|
+
if (!key) throw new Error(`Model ${this.model.name} has no single-column primary key`);
|
|
810
|
+
return key;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
// src/model/model.ts
|
|
814
|
+
/**
|
|
815
|
+
* ThinkPHP 风格模型,表结构来自 drizzle:
|
|
816
|
+
*
|
|
817
|
+
* ```ts
|
|
818
|
+
* export class User extends Model.define(users) {
|
|
819
|
+
* static override softDelete = true
|
|
820
|
+
* static override hidden = ['password']
|
|
821
|
+
*
|
|
822
|
+
* posts() {
|
|
823
|
+
* return this.hasMany(Post, 'userId')
|
|
824
|
+
* }
|
|
825
|
+
*
|
|
826
|
+
* static scopeActive(query: ModelQuery<User>) {
|
|
827
|
+
* query.where('status', 1)
|
|
828
|
+
* }
|
|
829
|
+
* }
|
|
830
|
+
*
|
|
831
|
+
* const user = await User.findOrFail(1)
|
|
832
|
+
* user.name = 'Ada'
|
|
833
|
+
* await user.save()
|
|
834
|
+
* await User.where('status', 1).with('posts').order('id', 'desc').paginate(1, 20)
|
|
835
|
+
* ```
|
|
836
|
+
*
|
|
837
|
+
* 访问器 / 修改器:定义 `getNameAttr(value, data)` / `setNameAttr(value, data)`;
|
|
838
|
+
* 事件:定义 `onBeforeInsert()`、`onAfterUpdate()` 等方法。
|
|
839
|
+
*/
|
|
840
|
+
var Model = class extends ActiveRecord {
|
|
841
|
+
/** 使用的连接名,默认使用 config/database.ts 的 default */
|
|
842
|
+
static connection = void 0;
|
|
843
|
+
/** 软删除:true 使用 deletedAt 列,或指定列名 */
|
|
844
|
+
static softDelete = false;
|
|
845
|
+
/** 自动维护 createdAt / updatedAt(列存在时),可指定列名或设为 false */
|
|
846
|
+
static timestamps = true;
|
|
847
|
+
/** 声明模型对应的表:`class User extends Model.define(users) {}` */
|
|
848
|
+
static define(table) {
|
|
849
|
+
class Defined extends Model {
|
|
850
|
+
static table = table;
|
|
851
|
+
}
|
|
852
|
+
for (const key of Object.keys(getTableColumns(table))) Object.defineProperty(Defined.prototype, key, {
|
|
853
|
+
get() {
|
|
854
|
+
return this.getAttr(key);
|
|
855
|
+
},
|
|
856
|
+
set(value) {
|
|
857
|
+
this.setAttr(key, value);
|
|
858
|
+
},
|
|
859
|
+
configurable: true,
|
|
860
|
+
enumerable: false
|
|
861
|
+
});
|
|
862
|
+
return Defined;
|
|
863
|
+
}
|
|
864
|
+
static primaryKeyName() {
|
|
865
|
+
return metaOf(this).primaryKey;
|
|
866
|
+
}
|
|
867
|
+
static query() {
|
|
868
|
+
return new ModelQuery(this);
|
|
869
|
+
}
|
|
870
|
+
static where(...args) {
|
|
871
|
+
return new ModelQuery(this).where(...args);
|
|
872
|
+
}
|
|
873
|
+
static whereIn(column, values) {
|
|
874
|
+
return new ModelQuery(this).whereIn(column, values);
|
|
875
|
+
}
|
|
876
|
+
static order(column, direction = "asc") {
|
|
877
|
+
return new ModelQuery(this).order(column, direction);
|
|
878
|
+
}
|
|
879
|
+
static with(...relations) {
|
|
880
|
+
return new ModelQuery(this).with(...relations);
|
|
881
|
+
}
|
|
882
|
+
static scope(name, ...args) {
|
|
883
|
+
return new ModelQuery(this).scope(name, ...args);
|
|
884
|
+
}
|
|
885
|
+
static withTrashed() {
|
|
886
|
+
return new ModelQuery(this).withTrashed();
|
|
887
|
+
}
|
|
888
|
+
static onlyTrashed() {
|
|
889
|
+
return new ModelQuery(this).onlyTrashed();
|
|
890
|
+
}
|
|
891
|
+
static find(id) {
|
|
892
|
+
return new ModelQuery(this).find(id);
|
|
893
|
+
}
|
|
894
|
+
static findOrFail(id) {
|
|
895
|
+
return new ModelQuery(this).findOrFail(id);
|
|
896
|
+
}
|
|
897
|
+
static all() {
|
|
898
|
+
return new ModelQuery(this).select();
|
|
899
|
+
}
|
|
900
|
+
static paginate(page, perPage) {
|
|
901
|
+
return new ModelQuery(this).paginate(page, perPage);
|
|
902
|
+
}
|
|
903
|
+
static count() {
|
|
904
|
+
return new ModelQuery(this).count();
|
|
905
|
+
}
|
|
906
|
+
/** 创建并保存(会触发事件与时间戳) */
|
|
907
|
+
static async create(data) {
|
|
908
|
+
const model = new this();
|
|
909
|
+
const record = model;
|
|
910
|
+
record.fill(data);
|
|
911
|
+
await record.save();
|
|
912
|
+
return model;
|
|
913
|
+
}
|
|
914
|
+
/** 按主键删除(逐个触发事件与软删除),返回删除数量 */
|
|
915
|
+
static async destroy(ids) {
|
|
916
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
917
|
+
const models = list.length ? await new ModelQuery(this).whereIn(defaultKey(this), list).select() : [];
|
|
918
|
+
for (const model of models) await model.delete();
|
|
919
|
+
return models.length;
|
|
920
|
+
}
|
|
921
|
+
/** 新模型插入,已存在模型更新修改过的字段 */
|
|
922
|
+
async save() {
|
|
923
|
+
const meta = this.#meta();
|
|
924
|
+
await this.runHook("onBeforeWrite");
|
|
925
|
+
if (!this.$exists) {
|
|
926
|
+
await this.runHook("onBeforeInsert");
|
|
927
|
+
for (const key of [meta.createdAt, meta.updatedAt]) if (key && this.$attributes[key] == null) this.$attributes[key] = nowFor(meta.columns[key]);
|
|
928
|
+
const [row] = await this.#builder().insert(pickColumns(this.$attributes, meta.columns));
|
|
929
|
+
if (row) this.$attributes = {
|
|
930
|
+
...this.$attributes,
|
|
931
|
+
...row
|
|
932
|
+
};
|
|
933
|
+
this.$exists = true;
|
|
934
|
+
this.syncOriginal();
|
|
935
|
+
await this.runHook("onAfterInsert");
|
|
936
|
+
} else {
|
|
937
|
+
const dirty = pickColumns(this.getDirty(), meta.columns);
|
|
938
|
+
if (Object.keys(dirty).length) {
|
|
939
|
+
await this.runHook("onBeforeUpdate");
|
|
940
|
+
if (meta.updatedAt && !(meta.updatedAt in dirty)) dirty[meta.updatedAt] = this.$attributes[meta.updatedAt] = nowFor(meta.columns[meta.updatedAt]);
|
|
941
|
+
await this.#whereKey().update(dirty);
|
|
942
|
+
this.syncOriginal();
|
|
943
|
+
await this.runHook("onAfterUpdate");
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
await this.runHook("onAfterWrite");
|
|
947
|
+
return this;
|
|
948
|
+
}
|
|
949
|
+
/** 删除:启用软删除时只标记删除时间 */
|
|
950
|
+
async delete() {
|
|
951
|
+
if (!this.$exists) return false;
|
|
952
|
+
const meta = this.#meta();
|
|
953
|
+
await this.runHook("onBeforeDelete");
|
|
954
|
+
if (meta.softDelete) {
|
|
955
|
+
const value = nowFor(meta.columns[meta.softDelete]);
|
|
956
|
+
await this.#whereKey().update({ [meta.softDelete]: value });
|
|
957
|
+
this.$attributes[meta.softDelete] = value;
|
|
958
|
+
this.$original[meta.softDelete] = value;
|
|
959
|
+
} else {
|
|
960
|
+
await this.#whereKey().delete();
|
|
961
|
+
this.$exists = false;
|
|
962
|
+
}
|
|
963
|
+
await this.runHook("onAfterDelete");
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
/** 物理删除 */
|
|
967
|
+
async forceDelete() {
|
|
968
|
+
if (!this.$exists) return false;
|
|
969
|
+
await this.runHook("onBeforeDelete");
|
|
970
|
+
await this.#whereKey().delete();
|
|
971
|
+
this.$exists = false;
|
|
972
|
+
await this.runHook("onAfterDelete");
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
/** 恢复软删除 */
|
|
976
|
+
async restore() {
|
|
977
|
+
const { softDelete } = this.#meta();
|
|
978
|
+
if (!softDelete) return this;
|
|
979
|
+
await this.runHook("onBeforeRestore");
|
|
980
|
+
await this.#whereKey().update({ [softDelete]: null });
|
|
981
|
+
this.$attributes[softDelete] = null;
|
|
982
|
+
this.$original[softDelete] = null;
|
|
983
|
+
await this.runHook("onAfterRestore");
|
|
984
|
+
return this;
|
|
985
|
+
}
|
|
986
|
+
trashed() {
|
|
987
|
+
const { softDelete } = this.#meta();
|
|
988
|
+
return softDelete ? this.$attributes[softDelete] != null : false;
|
|
989
|
+
}
|
|
990
|
+
/** 从数据库重新读取 */
|
|
991
|
+
async refresh() {
|
|
992
|
+
const row = await this.#whereKey().find();
|
|
993
|
+
if (!row) throw new DatabaseError(`${this.constructor.name} ${String(this.getKey())} no longer exists`);
|
|
994
|
+
this.$attributes = { ...row };
|
|
995
|
+
this.syncOriginal();
|
|
996
|
+
return this;
|
|
997
|
+
}
|
|
998
|
+
acceptsAttribute(key) {
|
|
999
|
+
return key in this.#meta().columns;
|
|
1000
|
+
}
|
|
1001
|
+
#meta() {
|
|
1002
|
+
return metaOf(this.constructor);
|
|
1003
|
+
}
|
|
1004
|
+
#builder() {
|
|
1005
|
+
const model = this.constructor;
|
|
1006
|
+
return new QueryBuilder(model.table, { connection: model.connection });
|
|
1007
|
+
}
|
|
1008
|
+
#whereKey() {
|
|
1009
|
+
const key = this.#meta().primaryKey;
|
|
1010
|
+
if (!key) throw new DatabaseError(`${this.constructor.name} has no single-column primary key`);
|
|
1011
|
+
return this.#builder().where(key, this.$original[key] ?? this.$attributes[key]);
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
/** 由数据库行构造模型(供 ModelQuery 使用) */
|
|
1015
|
+
function hydrateModel(model, row) {
|
|
1016
|
+
return Model.hydrate.call(model, row);
|
|
1017
|
+
}
|
|
1018
|
+
function pickColumns(values, columns) {
|
|
1019
|
+
return Object.fromEntries(Object.entries(values).filter(([key]) => key in columns));
|
|
1020
|
+
}
|
|
1021
|
+
// src/provider.ts
|
|
1022
|
+
/**
|
|
1023
|
+
* 数据库服务提供者(放在 app/provider/DatabaseProvider.ts):
|
|
1024
|
+
*
|
|
1025
|
+
* ```ts
|
|
1026
|
+
* export default databaseProvider()
|
|
1027
|
+
* ```
|
|
1028
|
+
*
|
|
1029
|
+
* 注册 DatabaseManager(可 inject),并让 Db / Model 使用该配置。连接在首次查询时建立,
|
|
1030
|
+
* 开发时跨热更新复用,应用关闭时释放。
|
|
1031
|
+
*/
|
|
1032
|
+
function databaseProvider(options = {}) {
|
|
1033
|
+
return { register(container, app) {
|
|
1034
|
+
const input = options.config ?? app.config.get("database", void 0);
|
|
1035
|
+
if (!input) throw new Error("databaseProvider(): config/database.ts is missing");
|
|
1036
|
+
const manager = new DatabaseManager(typeof input === "function" ? input() : input, {
|
|
1037
|
+
logger: container.make(Logger),
|
|
1038
|
+
rootDir: app.rootDir
|
|
1039
|
+
});
|
|
1040
|
+
container.instance(DatabaseManager, manager);
|
|
1041
|
+
setDatabaseManager(manager);
|
|
1042
|
+
} };
|
|
1043
|
+
}
|
|
1044
|
+
// src/testing.ts
|
|
1045
|
+
/**
|
|
1046
|
+
* 测试用:重建连接并执行迁移。配合 `.env.test` 中的 `DATABASE_URL=:memory:`,每个用例得到全新的数据库。
|
|
1047
|
+
*
|
|
1048
|
+
* ```ts
|
|
1049
|
+
* const t = useTestApp()
|
|
1050
|
+
* beforeEach(() => refreshDatabase())
|
|
1051
|
+
* ```
|
|
1052
|
+
*/
|
|
1053
|
+
async function refreshDatabase(options = {}) {
|
|
1054
|
+
const manager = useDatabaseManager();
|
|
1055
|
+
const name = options.connection ?? manager.defaultConnection;
|
|
1056
|
+
if (manager.connectionConfig(name).driver === "mongodb") {
|
|
1057
|
+
await (await manager.connection(name)).db.dropDatabase();
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
await manager.close(name);
|
|
1061
|
+
await migrate({
|
|
1062
|
+
manager,
|
|
1063
|
+
connection: options.connection,
|
|
1064
|
+
folder: resolve(process.cwd(), options.migrations ?? "database/migrations")
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
export { ActiveRecord, BelongsTo, Conditions, DatabaseError, DatabaseManager, Db, HasMany, HasOne, Model, ModelNotFoundException, ModelQuery, QueryBuilder, Relation, UniqueConstraintException, databaseProvider, defineDatabaseConfig, dialectOf, migrate, migrationsFolder, modelUserProvider, refreshDatabase, resolveSession, runMigrations, setDatabaseManager, transaction, translateDatabaseError, useDatabaseManager };
|