@bunyad/orm 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/casts.d.ts +50 -0
- package/dist/casts.js +309 -0
- package/dist/concerns.d.ts +64 -0
- package/dist/concerns.js +216 -0
- package/dist/decorators.d.ts +7 -0
- package/dist/decorators.js +12 -0
- package/dist/eager-fetch.d.ts +7 -0
- package/dist/eager-fetch.js +38 -0
- package/dist/eager.d.ts +24 -0
- package/dist/eager.js +854 -0
- package/dist/factory.d.ts +68 -0
- package/dist/factory.js +265 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/model-events.d.ts +43 -0
- package/dist/model-events.js +138 -0
- package/dist/model-helpers.d.ts +32 -0
- package/dist/model-helpers.js +99 -0
- package/dist/model-query.d.ts +312 -0
- package/dist/model-query.js +3170 -0
- package/dist/model-strictness.d.ts +35 -0
- package/dist/model-strictness.js +114 -0
- package/dist/model.d.ts +490 -0
- package/dist/model.js +1931 -0
- package/dist/orm-collection.d.ts +37 -0
- package/dist/orm-collection.js +150 -0
- package/dist/pivot.d.ts +9 -0
- package/dist/pivot.js +9 -0
- package/dist/relations.d.ts +335 -0
- package/dist/relations.js +1164 -0
- package/dist/scopes.d.ts +36 -0
- package/dist/scopes.js +128 -0
- package/package.json +49 -0
package/dist/concerns.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { morphMap } from "./model.js";
|
|
2
|
+
const HAS_UUIDS = Symbol.for("bunyad.orm.hasUuids");
|
|
3
|
+
const HAS_ULIDS = Symbol.for("bunyad.orm.hasUlids");
|
|
4
|
+
const PRUNABLE = Symbol.for("bunyad.orm.prunable");
|
|
5
|
+
const MASS_PRUNABLE = Symbol.for("bunyad.orm.massPrunable");
|
|
6
|
+
const prunableModels = new Set();
|
|
7
|
+
const massPrunableModels = new Set();
|
|
8
|
+
/** Crockford Base32 alphabet (ULID). */
|
|
9
|
+
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
10
|
+
/** Generate a UUIDv7 (time-ordered) string. */
|
|
11
|
+
export function uuid7() {
|
|
12
|
+
const bytes = new Uint8Array(16);
|
|
13
|
+
crypto.getRandomValues(bytes);
|
|
14
|
+
const ms = BigInt(Date.now());
|
|
15
|
+
bytes[0] = Number((ms >> 40n) & 0xffn);
|
|
16
|
+
bytes[1] = Number((ms >> 32n) & 0xffn);
|
|
17
|
+
bytes[2] = Number((ms >> 24n) & 0xffn);
|
|
18
|
+
bytes[3] = Number((ms >> 16n) & 0xffn);
|
|
19
|
+
bytes[4] = Number((ms >> 8n) & 0xffn);
|
|
20
|
+
bytes[5] = Number(ms & 0xffn);
|
|
21
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
22
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
23
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
24
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
25
|
+
}
|
|
26
|
+
/** Generate a 26-character ULID. */
|
|
27
|
+
export function ulid(now = Date.now()) {
|
|
28
|
+
let time = now;
|
|
29
|
+
let out = "";
|
|
30
|
+
for (let i = 0; i < 10; i++) {
|
|
31
|
+
out = CROCKFORD[time % 32] + out;
|
|
32
|
+
time = Math.floor(time / 32);
|
|
33
|
+
}
|
|
34
|
+
const rand = new Uint8Array(16);
|
|
35
|
+
crypto.getRandomValues(rand);
|
|
36
|
+
for (let i = 0; i < 16; i++) {
|
|
37
|
+
out += CROCKFORD[rand[i] % 32];
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
function asConcern(target) {
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
function assignUniqueIds(model, ctor) {
|
|
45
|
+
const keys = ctor.uniqueIds?.() ?? [ctor.primaryKey];
|
|
46
|
+
const row = model;
|
|
47
|
+
for (const key of keys) {
|
|
48
|
+
if (row[key] == null || row[key] === "") {
|
|
49
|
+
row[key] = ctor.newUniqueId?.() ?? uuid7();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Ordered UUID primary keys (`incrementing = false`).
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* @HasUuids()
|
|
59
|
+
* class Article extends Model {}
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function HasUuids() {
|
|
63
|
+
return (target) => {
|
|
64
|
+
const ctor = asConcern(target);
|
|
65
|
+
ctor[HAS_UUIDS] = true;
|
|
66
|
+
ctor.incrementing = false;
|
|
67
|
+
ctor.keyType = "string";
|
|
68
|
+
ctor.newUniqueId = () => uuid7();
|
|
69
|
+
ctor.uniqueIds = () => [ctor.primaryKey];
|
|
70
|
+
morphMap({ [ctor.name]: ctor });
|
|
71
|
+
ctor.creating((model) => assignUniqueIds(model, ctor));
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* ULID primary keys (`incrementing = false`).
|
|
76
|
+
*/
|
|
77
|
+
export function HasUlids() {
|
|
78
|
+
return (target) => {
|
|
79
|
+
const ctor = asConcern(target);
|
|
80
|
+
ctor[HAS_ULIDS] = true;
|
|
81
|
+
ctor.incrementing = false;
|
|
82
|
+
ctor.keyType = "string";
|
|
83
|
+
ctor.newUniqueId = () => ulid();
|
|
84
|
+
ctor.uniqueIds = () => [ctor.primaryKey];
|
|
85
|
+
morphMap({ [ctor.name]: ctor });
|
|
86
|
+
ctor.creating((model) => assignUniqueIds(model, ctor));
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function resolveFactory(factory) {
|
|
90
|
+
if (typeof factory === "function" &&
|
|
91
|
+
factory.prototype != null &&
|
|
92
|
+
typeof factory.prototype.model === "function") {
|
|
93
|
+
return new factory();
|
|
94
|
+
}
|
|
95
|
+
return factory();
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Bind a factory class and expose `Model.factory()`.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* @HasFactory(UserFactory)
|
|
103
|
+
* // or @HasFactory(() => new UserFactory())
|
|
104
|
+
* class User extends Model {}
|
|
105
|
+
* User.factory().create();
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
export function HasFactory(factory) {
|
|
109
|
+
return (target) => {
|
|
110
|
+
const ctor = asConcern(target);
|
|
111
|
+
ctor.factory = () => resolveFactory(factory);
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Soft deletes — sets `static softDeletes = true`.
|
|
116
|
+
* Prefer this decorator for `use SoftDeletes`-style call sites.
|
|
117
|
+
*/
|
|
118
|
+
export function SoftDeletes(column = "deleted_at") {
|
|
119
|
+
return (target) => {
|
|
120
|
+
const ctor = asConcern(target);
|
|
121
|
+
ctor.softDeletes = true;
|
|
122
|
+
ctor.deletedAt = column;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Register for `model:prune` / `pruneAll()`.
|
|
127
|
+
* Define `static prunable()` returning the query of models to delete.
|
|
128
|
+
*/
|
|
129
|
+
export function Prunable() {
|
|
130
|
+
return (target) => {
|
|
131
|
+
const ctor = asConcern(target);
|
|
132
|
+
ctor[PRUNABLE] = true;
|
|
133
|
+
prunableModels.add(ctor);
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Mass-delete via query (no model hydration / events).
|
|
138
|
+
*/
|
|
139
|
+
export function MassPrunable() {
|
|
140
|
+
return (target) => {
|
|
141
|
+
const ctor = asConcern(target);
|
|
142
|
+
ctor[MASS_PRUNABLE] = true;
|
|
143
|
+
massPrunableModels.add(ctor);
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function prunableModelClasses() {
|
|
147
|
+
return [...prunableModels];
|
|
148
|
+
}
|
|
149
|
+
export function massPrunableModelClasses() {
|
|
150
|
+
return [...massPrunableModels];
|
|
151
|
+
}
|
|
152
|
+
/** Clear registries (tests). */
|
|
153
|
+
export function clearPrunableRegistries() {
|
|
154
|
+
prunableModels.clear();
|
|
155
|
+
massPrunableModels.clear();
|
|
156
|
+
}
|
|
157
|
+
async function pruneOne(ModelClass, options = {}) {
|
|
158
|
+
const ctor = ModelClass;
|
|
159
|
+
if (typeof ctor.prunable !== "function") {
|
|
160
|
+
throw new Error(`Model [${ModelClass.name}] is prunable but does not define static prunable().`);
|
|
161
|
+
}
|
|
162
|
+
const chunk = options.chunk ?? 1000;
|
|
163
|
+
let total = 0;
|
|
164
|
+
if (options.mass || ModelClass[MASS_PRUNABLE]) {
|
|
165
|
+
const query = await ctor.prunable();
|
|
166
|
+
if (ModelClass.softDeletes) {
|
|
167
|
+
total += await query.forceDelete();
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
total += await query.delete();
|
|
171
|
+
}
|
|
172
|
+
return total;
|
|
173
|
+
}
|
|
174
|
+
for (;;) {
|
|
175
|
+
const query = await ctor.prunable();
|
|
176
|
+
const models = await query.limit(chunk).get();
|
|
177
|
+
if (models.isEmpty())
|
|
178
|
+
break;
|
|
179
|
+
for (const model of models) {
|
|
180
|
+
await ctor.pruning?.(model);
|
|
181
|
+
if (ModelClass.softDeletes) {
|
|
182
|
+
await model.forceDelete();
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
await model.delete();
|
|
186
|
+
}
|
|
187
|
+
total += 1;
|
|
188
|
+
}
|
|
189
|
+
if (models.length < chunk)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
return total;
|
|
193
|
+
}
|
|
194
|
+
/** Prune one model class (`Model::pruneAll` for a single class). */
|
|
195
|
+
export async function prune(ModelClass, options = {}) {
|
|
196
|
+
return pruneOne(ModelClass, options);
|
|
197
|
+
}
|
|
198
|
+
/** Prune every registered `@Prunable` / `@MassPrunable` model. */
|
|
199
|
+
export async function pruneAll(options = {}) {
|
|
200
|
+
const results = [];
|
|
201
|
+
for (const ModelClass of massPrunableModels) {
|
|
202
|
+
results.push({
|
|
203
|
+
model: ModelClass.name,
|
|
204
|
+
count: await pruneOne(ModelClass, { ...options, mass: true }),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
for (const ModelClass of prunableModels) {
|
|
208
|
+
if (massPrunableModels.has(ModelClass))
|
|
209
|
+
continue;
|
|
210
|
+
results.push({
|
|
211
|
+
model: ModelClass.name,
|
|
212
|
+
count: await pruneOne(ModelClass, options),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return results;
|
|
216
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ModelClass, ModelQuery, ModelQueryOptions } from "./model.js";
|
|
2
|
+
export type ModelBuilderConstructor = new (model: ModelClass, options?: ModelQueryOptions) => ModelQuery;
|
|
3
|
+
/**
|
|
4
|
+
* Use a custom `ModelQuery` subclass for this model’s `newQuery()` / `where()` / …
|
|
5
|
+
*/
|
|
6
|
+
export declare function UseBuilder(builder: ModelBuilderConstructor): ClassDecorator;
|
|
7
|
+
export declare function resolveModelBuilder(model: Function): ModelBuilderConstructor | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const MODEL_BUILDER = Symbol.for("bunyad.orm.modelBuilder");
|
|
2
|
+
/**
|
|
3
|
+
* Use a custom `ModelQuery` subclass for this model’s `newQuery()` / `where()` / …
|
|
4
|
+
*/
|
|
5
|
+
export function UseBuilder(builder) {
|
|
6
|
+
return (target) => {
|
|
7
|
+
target[MODEL_BUILDER] = builder;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export function resolveModelBuilder(model) {
|
|
11
|
+
return model[MODEL_BUILDER];
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Model, type ModelClass } from "./model.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetch related models by key IN (...) for eager load.
|
|
4
|
+
* Prefer raw SQL + light hydrate (matches ofMany) when the related model has
|
|
5
|
+
* no global scopes; fall back to ModelQuery when scopes must run.
|
|
6
|
+
*/
|
|
7
|
+
export declare function eagerFetchByKeys(Related: ModelClass, keyColumn: string, ids: unknown[]): Promise<Model[]>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared key IN (...) fetch for eager load and HasManyThrough lazy get.
|
|
3
|
+
*/
|
|
4
|
+
import { wrapSqlName } from "@bunyad/database";
|
|
5
|
+
import { hasGlobalScopes } from "./scopes.js";
|
|
6
|
+
import { softDeleteAliasSql } from "./model-helpers.js";
|
|
7
|
+
import { Model } from "./model.js";
|
|
8
|
+
/**
|
|
9
|
+
* Fetch related models by key IN (...) for eager load.
|
|
10
|
+
* Prefer raw SQL + light hydrate (matches ofMany) when the related model has
|
|
11
|
+
* no global scopes; fall back to ModelQuery when scopes must run.
|
|
12
|
+
*/
|
|
13
|
+
export async function eagerFetchByKeys(Related, keyColumn, ids) {
|
|
14
|
+
if (ids.length === 0)
|
|
15
|
+
return [];
|
|
16
|
+
if (hasGlobalScopes(Related)) {
|
|
17
|
+
const collection = await Related.whereIn(keyColumn, ids).get();
|
|
18
|
+
return collection.all();
|
|
19
|
+
}
|
|
20
|
+
const conn = Related.getConnection();
|
|
21
|
+
const dialect = conn.dialect;
|
|
22
|
+
const table = wrapSqlName(dialect, Related.table);
|
|
23
|
+
const key = wrapSqlName(dialect, keyColumn);
|
|
24
|
+
const soft = softDeleteAliasSql(Related, table);
|
|
25
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
26
|
+
const sql = `SELECT ${table}.* FROM ${table} WHERE ${table}.${key} IN (${placeholders})${soft}`;
|
|
27
|
+
// Prefer sync SQLite reads — matches BelongsTo().first() → find() hot path.
|
|
28
|
+
const rows = (conn.allSync
|
|
29
|
+
? conn.allSync(sql, ids)
|
|
30
|
+
: await conn.all(sql, ids));
|
|
31
|
+
const models = new Array(rows.length);
|
|
32
|
+
for (let i = 0; i < rows.length; i++) {
|
|
33
|
+
const model = new Related(rows[i]);
|
|
34
|
+
model.exists = true;
|
|
35
|
+
models[i] = model;
|
|
36
|
+
}
|
|
37
|
+
return models;
|
|
38
|
+
}
|
package/dist/eager.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type NormalizedEagerRelation } from "./model-helpers.js";
|
|
2
|
+
import { Model } from "./model.js";
|
|
3
|
+
/** Eager-load named relations onto models (`with` / `load` / collections).
|
|
4
|
+
* Batches BelongsTo / HasMany / HasOne / BelongsToMany / Morph* / ofMany with
|
|
5
|
+
* `WHERE IN`. Nested paths like `parent.children` are supported.
|
|
6
|
+
* Independent top-level relations load in parallel.
|
|
7
|
+
* Constraints from `with({ posts: (q) => q.where(…) })` are applied per relation.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Eager-load relations onto models.
|
|
11
|
+
* Returns void when all work is synchronous (SQLite BelongsTo fast-find),
|
|
12
|
+
* otherwise a Promise — callers must branch like `get()` hydrate does.
|
|
13
|
+
*/
|
|
14
|
+
export declare function eagerLoadModels(models: Model[], relations: Array<string | NormalizedEagerRelation>): void | Promise<void>;
|
|
15
|
+
type AggregateFn = "count" | "sum" | "avg" | "min" | "max" | "exists";
|
|
16
|
+
export type EagerAggregateSpec = {
|
|
17
|
+
relation: string;
|
|
18
|
+
alias: string;
|
|
19
|
+
fn: AggregateFn;
|
|
20
|
+
column?: string;
|
|
21
|
+
};
|
|
22
|
+
/** Batch `loadCount` / `loadSum` / … onto many models (one GROUP BY query per relation). */
|
|
23
|
+
export declare function eagerLoadAggregates(models: Model[], specs: EagerAggregateSpec[]): Promise<void>;
|
|
24
|
+
export {};
|