@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.
@@ -0,0 +1,335 @@
1
+ import { ConflictException, NotFoundException } from "@hazejs/core";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { getTableColumns } from "drizzle-orm";
4
+ // src/errors.ts
5
+ /** findOrFail() 未找到数据,渲染为 404 */
6
+ var ModelNotFoundException = class extends NotFoundException {
7
+ model;
8
+ constructor(model, key) {
9
+ super(key === void 0 ? `${model} 不存在` : `${model} ${String(key)} 不存在`);
10
+ this.model = model;
11
+ }
12
+ };
13
+ var DatabaseError = class extends Error {
14
+ name = "DatabaseError";
15
+ };
16
+ /** 违反唯一约束(重复数据),渲染为 409 */
17
+ var UniqueConstraintException = class extends ConflictException {
18
+ constructor(cause) {
19
+ super("数据已存在(违反唯一约束)", { cause });
20
+ }
21
+ };
22
+ /** 把各数据库的唯一约束错误统一转换为 UniqueConstraintException,其余原样返回 */
23
+ function translateDatabaseError(error) {
24
+ for (let current = error, depth = 0; current && depth < 5; current = current.cause, depth++) {
25
+ const code = String(current.code ?? "");
26
+ const message = String(current.message ?? "");
27
+ if (code === "11000" || current.code === 11e3 || code === "23505" || code === "ER_DUP_ENTRY" || current.errno === 1062 || code.startsWith("SQLITE_CONSTRAINT_UNIQUE") || code === "SQLITE_CONSTRAINT_PRIMARYKEY" || /UNIQUE constraint failed/.test(message)) return new UniqueConstraintException(error);
28
+ }
29
+ return error;
30
+ }
31
+ // src/context.ts
32
+ let manager;
33
+ /** 由 databaseProvider() 在应用启动时设置;脚本 / 测试中可直接调用 */
34
+ function setDatabaseManager(value) {
35
+ manager = value;
36
+ }
37
+ function useDatabaseManager() {
38
+ if (!manager) throw new DatabaseError("Database is not configured. Add databaseProvider() in app/provider and create config/database.ts");
39
+ return manager;
40
+ }
41
+ /** 事务上下文:连接名 → 事务内的 drizzle 实例 */
42
+ const transactions = new AsyncLocalStorage();
43
+ async function resolveSession(connection) {
44
+ const resolved = await useDatabaseManager().connection(connection);
45
+ if (resolved.dialect === "mongodb") throw new DatabaseError(`Connection "${resolved.name}" is MongoDB; use Mongo / MongoModel from @hazejs/orm/mongo`);
46
+ return {
47
+ name: resolved.name,
48
+ driver: resolved.driver,
49
+ dialect: resolved.dialect,
50
+ db: transactions.getStore()?.get(resolved.name) ?? resolved.db
51
+ };
52
+ }
53
+ /**
54
+ * 在事务中执行;回调内通过 Db / Model 发起的查询自动加入该事务(隐式传播),
55
+ * 嵌套调用使用保存点。回调抛错时回滚。
56
+ */
57
+ async function transaction(callback, connection) {
58
+ const session = await resolveSession(connection);
59
+ if (session.driver === "d1") throw new DatabaseError(`Connection "${session.name}" is Cloudflare D1, which does not support interactive transactions; use (await Db.drizzle()).batch([...]) for atomic writes`);
60
+ const store = transactions.getStore();
61
+ return session.db.transaction((tx) => transactions.run(new Map(store).set(session.name, tx), () => callback(tx)));
62
+ }
63
+ // src/utils.ts
64
+ function isPlainObject(value) {
65
+ if (value === null || typeof value !== "object") return false;
66
+ const proto = Object.getPrototypeOf(value);
67
+ return proto === null || proto === Object.prototype;
68
+ }
69
+ const primaryKeys = /* @__PURE__ */ new WeakMap();
70
+ /** 单列主键的属性名(复合主键返回 undefined) */
71
+ function primaryKeyOf(table) {
72
+ if (!primaryKeys.has(table)) {
73
+ const keys = Object.entries(getTableColumns(table)).filter(([, column]) => column.primary).map(([key]) => key);
74
+ primaryKeys.set(table, keys.length === 1 ? keys[0] : void 0);
75
+ }
76
+ return primaryKeys.get(table);
77
+ }
78
+ /** 按列的数据类型生成“当前时间”:Date / ISO 字符串 / 毫秒数 */
79
+ function nowFor(column) {
80
+ const now = /* @__PURE__ */ new Date();
81
+ switch (column.dataType) {
82
+ case "string": return now.toISOString();
83
+ case "number": return now.getTime();
84
+ default: return now;
85
+ }
86
+ }
87
+ /** 各驱动的“影响行数”字段不同 */
88
+ function affectedRows(result) {
89
+ const value = result?.rowsAffected ?? result?.affectedRows ?? result?.[0]?.affectedRows ?? result?.meta?.changes ?? result?.count ?? result?.rowCount ?? 0;
90
+ return Number(value);
91
+ }
92
+ function pascalCase(value) {
93
+ return value.replace(/[-_\s]+(.)?/g, (_, char) => (char ?? "").toUpperCase()).replace(/^./, (char) => char.toUpperCase());
94
+ }
95
+ // src/model/relations.ts
96
+ function queryOf(related) {
97
+ return related.query();
98
+ }
99
+ /**
100
+ * 关联关系。可直接 await 获取结果,也可继续构造查询:
101
+ *
102
+ * ```ts
103
+ * const posts = await user.posts()
104
+ * const recent = await user.posts().query().order('id', 'desc').limit(5).select()
105
+ * ```
106
+ */
107
+ var Relation = class {
108
+ parent;
109
+ related;
110
+ constructor(parent, related) {
111
+ this.parent = parent;
112
+ this.related = related;
113
+ }
114
+ then(onFulfilled, onRejected) {
115
+ return this.get().then(onFulfilled, onRejected);
116
+ }
117
+ };
118
+ var HasOneOrMany = class extends Relation {
119
+ foreignKey;
120
+ localKey;
121
+ constructor(parent, related, foreignKey, localKey) {
122
+ super(parent, related);
123
+ this.foreignKey = foreignKey;
124
+ this.localKey = localKey;
125
+ }
126
+ query() {
127
+ return queryOf(this.related).where(this.foreignKey, this.parent.getRaw(this.localKey));
128
+ }
129
+ async loadFor(parents, nested) {
130
+ const keys = unique(parents.map((parent) => parent.getRaw(this.localKey)));
131
+ const groups = /* @__PURE__ */ new Map();
132
+ if (!keys.length) return groups;
133
+ const children = await queryOf(this.related).whereIn(this.foreignKey, keys).with(...nested).select();
134
+ for (const child of children) {
135
+ const key = keyOf(child.getRaw(this.foreignKey));
136
+ groups.set(key, [...groups.get(key) ?? [], child]);
137
+ }
138
+ return groups;
139
+ }
140
+ };
141
+ var HasMany = class extends HasOneOrMany {
142
+ get() {
143
+ return this.query().select();
144
+ }
145
+ async eagerLoad(parents, name, nested) {
146
+ const groups = await this.loadFor(parents, nested);
147
+ for (const parent of parents) parent.setRelation(name, groups.get(keyOf(parent.getRaw(this.localKey))) ?? []);
148
+ }
149
+ };
150
+ var HasOne = class extends HasOneOrMany {
151
+ async get() {
152
+ return await this.query().find() ?? null;
153
+ }
154
+ async eagerLoad(parents, name, nested) {
155
+ const groups = await this.loadFor(parents, nested);
156
+ for (const parent of parents) parent.setRelation(name, groups.get(keyOf(parent.getRaw(this.localKey)))?.[0] ?? null);
157
+ }
158
+ };
159
+ var BelongsTo = class extends Relation {
160
+ foreignKey;
161
+ ownerKey;
162
+ constructor(parent, related, foreignKey, ownerKey) {
163
+ super(parent, related);
164
+ this.foreignKey = foreignKey;
165
+ this.ownerKey = ownerKey;
166
+ }
167
+ query() {
168
+ return queryOf(this.related).where(this.ownerKey, this.parent.getRaw(this.foreignKey));
169
+ }
170
+ async get() {
171
+ if (this.parent.getRaw(this.foreignKey) == null) return null;
172
+ return await this.query().find() ?? null;
173
+ }
174
+ async eagerLoad(parents, name, nested) {
175
+ const keys = unique(parents.map((parent) => parent.getRaw(this.foreignKey)));
176
+ const owners = keys.length ? await queryOf(this.related).whereIn(this.ownerKey, keys).with(...nested).select() : [];
177
+ const byKey = new Map(owners.map((owner) => [keyOf(owner.getRaw(this.ownerKey)), owner]));
178
+ for (const parent of parents) parent.setRelation(name, byKey.get(keyOf(parent.getRaw(this.foreignKey))) ?? null);
179
+ }
180
+ };
181
+ /** 预加载:`['posts', 'posts.comments', 'profile']` 按第一段分组,其余作为嵌套关联 */
182
+ async function loadRelations(records, names) {
183
+ if (!records.length) return;
184
+ const tree = /* @__PURE__ */ new Map();
185
+ for (const name of names) {
186
+ const [head, ...rest] = name.split(".");
187
+ const nested = tree.get(head) ?? [];
188
+ if (rest.length) nested.push(rest.join("."));
189
+ tree.set(head, nested);
190
+ }
191
+ for (const [name, nested] of tree) {
192
+ const factory = records[0][name];
193
+ if (typeof factory !== "function") throw new TypeError(`Relation "${name}" is not defined on ${records[0].constructor.name}`);
194
+ const relation = factory.call(records[0]);
195
+ if (!(relation instanceof Relation)) throw new TypeError(`${records[0].constructor.name}.${name}() must return this.hasMany() / this.hasOne() / this.belongsTo()`);
196
+ await relation.eagerLoad(records, name, nested);
197
+ }
198
+ }
199
+ function defaultKey(record) {
200
+ const key = record.primaryKeyName();
201
+ if (!key) throw new Error(`${record.name} has no single-column primary key; pass the key explicitly`);
202
+ return key;
203
+ }
204
+ /** 关联键的比较值:ObjectId 等对象按字符串比较 */
205
+ function keyOf(value) {
206
+ if (value && typeof value === "object" && typeof value.toHexString === "function") return value.toHexString();
207
+ return value;
208
+ }
209
+ function unique(values) {
210
+ const seen = /* @__PURE__ */ new Map();
211
+ for (const value of values) if (value !== null && value !== void 0 && !seen.has(keyOf(value))) seen.set(keyOf(value), value);
212
+ return [...seen.values()];
213
+ }
214
+ // src/model/record.ts
215
+ /**
216
+ * SQL 模型(Model)与 MongoDB 模型(MongoModel)共用的基类:
217
+ * 属性与脏检查、访问器 / 修改器、关联、序列化与事件。
218
+ *
219
+ * 内部状态使用 `$` 前缀的受保护字段,避免与真实字段名冲突。
220
+ */
221
+ var ActiveRecord = class {
222
+ /** 序列化时隐藏的字段 */
223
+ static hidden = [];
224
+ /** 序列化时追加的虚拟字段(需定义 getXxxAttr) */
225
+ static append = [];
226
+ $attributes = {};
227
+ $original = {};
228
+ $relations = {};
229
+ $exists = false;
230
+ /** 主键字段名(子类实现) */
231
+ static primaryKeyName() {}
232
+ /** 由数据库行 / 文档构造已存在的记录 */
233
+ static hydrate(row) {
234
+ const record = new this();
235
+ record.$attributes = { ...row };
236
+ record.$original = { ...row };
237
+ record.$exists = true;
238
+ return record;
239
+ }
240
+ get exists() {
241
+ return this.$exists;
242
+ }
243
+ /** 主键值 */
244
+ getKey() {
245
+ const key = this.constructor.primaryKeyName();
246
+ return key ? this.$attributes[key] : void 0;
247
+ }
248
+ /** 读取属性(经过 getXxxAttr 访问器) */
249
+ getAttr(key) {
250
+ const accessor = this[`get${pascalCase(key)}Attr`];
251
+ const value = this.$attributes[key];
252
+ return typeof accessor === "function" ? accessor.call(this, value, this.$attributes) : value;
253
+ }
254
+ /** 写入属性(经过 setXxxAttr 修改器) */
255
+ setAttr(key, value) {
256
+ const mutator = this[`set${pascalCase(key)}Attr`];
257
+ this.$attributes[key] = typeof mutator === "function" ? mutator.call(this, value, this.$attributes) : value;
258
+ return this;
259
+ }
260
+ /** 原始属性(不经过访问器) */
261
+ getRaw(key) {
262
+ return this.$attributes[key];
263
+ }
264
+ /** 批量赋值(SQL 模型忽略表中不存在的列) */
265
+ fill(data) {
266
+ for (const [key, value] of Object.entries(data)) if (this.acceptsAttribute(key)) this.setAttr(key, value);
267
+ return this;
268
+ }
269
+ isDirty(key) {
270
+ const dirty = this.getDirty();
271
+ return key ? key in dirty : Object.keys(dirty).length > 0;
272
+ }
273
+ /** 自上次同步以来修改过的字段 */
274
+ getDirty() {
275
+ const dirty = {};
276
+ for (const [key, value] of Object.entries(this.$attributes)) if (!this.$exists || !sameValue(value, this.$original[key])) dirty[key] = value;
277
+ return dirty;
278
+ }
279
+ getOriginal(key) {
280
+ return key ? this.$original[key] : { ...this.$original };
281
+ }
282
+ /** 加载关联(可嵌套):`await user.load('posts.comments')` */
283
+ async load(...relations) {
284
+ await loadRelations([this], relations);
285
+ return this;
286
+ }
287
+ /** 已加载的关联结果 */
288
+ getRelation(name) {
289
+ return this.$relations[name];
290
+ }
291
+ setRelation(name, value) {
292
+ this.$relations[name] = value;
293
+ return this;
294
+ }
295
+ hasMany(related, foreignKey, localKey) {
296
+ return new HasMany(this, related, foreignKey, localKey ?? defaultKey(this.constructor));
297
+ }
298
+ hasOne(related, foreignKey, localKey) {
299
+ return new HasOne(this, related, foreignKey, localKey ?? defaultKey(this.constructor));
300
+ }
301
+ belongsTo(related, foreignKey, ownerKey) {
302
+ return new BelongsTo(this, related, foreignKey, ownerKey ?? defaultKey(related));
303
+ }
304
+ /** 序列化:应用访问器、隐藏字段、追加字段与已加载的关联 */
305
+ toJSON() {
306
+ const record = this.constructor;
307
+ const hidden = new Set(record.hidden);
308
+ const result = {};
309
+ for (const key of Object.keys(this.$attributes)) if (!hidden.has(key)) result[key] = this.getAttr(key);
310
+ for (const key of record.append) result[key] = this.getAttr(key);
311
+ for (const [key, value] of Object.entries(this.$relations)) if (!hidden.has(key)) result[key] = Array.isArray(value) ? value.map(serialize) : serialize(value);
312
+ return result;
313
+ }
314
+ /** 是否接受批量赋值的字段 */
315
+ acceptsAttribute(_key) {
316
+ return true;
317
+ }
318
+ async runHook(name) {
319
+ const hook = this[name];
320
+ if (typeof hook === "function") await hook.call(this);
321
+ }
322
+ /** 写入成功后同步原始值 */
323
+ syncOriginal() {
324
+ this.$original = { ...this.$attributes };
325
+ }
326
+ };
327
+ function serialize(value) {
328
+ return value instanceof ActiveRecord ? value.toJSON() : value;
329
+ }
330
+ function sameValue(a, b) {
331
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
332
+ if (a && b && typeof a === "object" && typeof a.equals === "function") return a.equals(b);
333
+ return Object.is(a, b);
334
+ }
335
+ export { DatabaseError as _, Relation as a, translateDatabaseError as b, affectedRows as c, pascalCase as d, primaryKeyOf as f, useDatabaseManager as g, transaction as h, HasOne as i, isPlainObject as l, setDatabaseManager as m, BelongsTo as n, defaultKey as o, resolveSession as p, HasMany as r, loadRelations as s, ActiveRecord as t, nowFor as u, ModelNotFoundException as v, UniqueConstraintException as y };
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@hazejs/orm",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "type": "module",
4
+ "version": "0.1.2",
5
+ "description": "Models, query builder and migrations for hazejs on top of drizzle (MySQL, PostgreSQL, SQLite, Turso, D1, MongoDB)",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/penna-org/haze/tree/main/packages/orm#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/penna-org/haze.git",
11
+ "directory": "packages/orm"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/penna-org/haze/issues"
15
+ },
16
+ "keywords": [
17
+ "hazejs",
18
+ "haze",
19
+ "nodejs",
20
+ "typescript",
21
+ "orm",
22
+ "drizzle",
23
+ "mysql",
24
+ "postgresql",
25
+ "sqlite",
26
+ "turso",
27
+ "d1",
28
+ "mongodb",
29
+ "active-record"
30
+ ],
31
+ "sideEffects": false,
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "default": "./dist/index.mjs"
36
+ },
37
+ "./mongo": {
38
+ "types": "./dist/mongo/index.d.mts",
39
+ "default": "./dist/mongo/index.mjs"
40
+ },
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "dist"
45
+ ],
46
+ "engines": {
47
+ "node": ">=22.12.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@electric-sql/pglite": "^0.5.0",
51
+ "@libsql/client": "^0.18.0",
52
+ "drizzle-orm": "^0.45.0",
53
+ "mongodb": "^7.0.0",
54
+ "mysql2": "^3.0.0",
55
+ "postgres": "^3.4.0",
56
+ "@hazejs/core": "^0.1.2"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@electric-sql/pglite": {
60
+ "optional": true
61
+ },
62
+ "@libsql/client": {
63
+ "optional": true
64
+ },
65
+ "mongodb": {
66
+ "optional": true
67
+ },
68
+ "mysql2": {
69
+ "optional": true
70
+ },
71
+ "postgres": {
72
+ "optional": true
73
+ }
74
+ },
75
+ "devDependencies": {
76
+ "@electric-sql/pglite": "^0.5.12",
77
+ "@libsql/client": "^0.18.0",
78
+ "drizzle-orm": "^0.45.4",
79
+ "miniflare": "^4.20260730.0",
80
+ "mongodb": "^7.7.0",
81
+ "mongodb-memory-server-core": "^11.3.0",
82
+ "mysql2": "^3.24.6",
83
+ "postgres": "^3.4.9",
84
+ "@hazejs/core": "0.1.2"
85
+ },
86
+ "scripts": {
87
+ "build": "tsdown"
88
+ }
6
89
  }