@bunyad/orm 0.2.0-beta.1 → 0.2.0-beta.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/README.md +1 -2
- package/dist/casts.d.ts +16 -3
- package/dist/casts.js +150 -4
- package/dist/eager.js +130 -11
- package/dist/factory.d.ts +20 -1
- package/dist/factory.js +143 -37
- package/dist/index.d.ts +21 -1
- package/dist/index.js +3 -1
- package/dist/model-events.d.ts +8 -2
- package/dist/model-events.js +27 -4
- package/dist/model-query.d.ts +75 -29
- package/dist/model-query.js +252 -133
- package/dist/model.d.ts +164 -34
- package/dist/model.js +708 -44
- package/dist/relations.d.ts +187 -11
- package/dist/relations.js +815 -89
- package/dist/schema-types.d.ts +27 -0
- package/dist/schema-types.js +145 -0
- package/dist/test-drivers.d.ts +10 -0
- package/dist/test-drivers.js +40 -0
- package/dist/typed-names.d.ts +53 -0
- package/dist/typed-names.js +4 -0
- package/package.json +3 -3
package/dist/factory.js
CHANGED
|
@@ -83,6 +83,11 @@ export class Factory {
|
|
|
83
83
|
#has = [];
|
|
84
84
|
#for = [];
|
|
85
85
|
#hasAttached = [];
|
|
86
|
+
#configured = false;
|
|
87
|
+
/** Models reused instead of creating new parents (`recycle`). */
|
|
88
|
+
#recycled = [];
|
|
89
|
+
/** Parents created from `for(Factory)` — one per batch, shared by every model in it. */
|
|
90
|
+
#forParents = new Map();
|
|
86
91
|
/** Public model class accessor for relationship factories. */
|
|
87
92
|
modelClass() {
|
|
88
93
|
return this.model();
|
|
@@ -90,6 +95,10 @@ export class Factory {
|
|
|
90
95
|
definition() {
|
|
91
96
|
return {};
|
|
92
97
|
}
|
|
98
|
+
/** Runs once, before the first `make` / `create` (`afterMaking`, default states, …). */
|
|
99
|
+
configure() {
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
93
102
|
/** Number of models to make/create. */
|
|
94
103
|
count(amount) {
|
|
95
104
|
this.#count = amount;
|
|
@@ -140,6 +149,26 @@ export class Factory {
|
|
|
140
149
|
this.#hasAttached.push({ related, pivot, relationship });
|
|
141
150
|
return this;
|
|
142
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* `recycle` — reuse existing models when this factory (or its nested `for` /
|
|
154
|
+
* `has` factories) would otherwise create a parent of the same class.
|
|
155
|
+
*/
|
|
156
|
+
recycle(models) {
|
|
157
|
+
const list = Array.isArray(models)
|
|
158
|
+
? models
|
|
159
|
+
: "all" in models && typeof models.all === "function"
|
|
160
|
+
? models.all()
|
|
161
|
+
: [models];
|
|
162
|
+
this.#recycled.push(...list);
|
|
163
|
+
return this;
|
|
164
|
+
}
|
|
165
|
+
/** A recycled model of `Ctor`, chosen at random. */
|
|
166
|
+
#recycledFor(Ctor) {
|
|
167
|
+
const matches = this.#recycled.filter((m) => m.constructor === Ctor);
|
|
168
|
+
if (matches.length === 0)
|
|
169
|
+
return undefined;
|
|
170
|
+
return matches[Math.floor(Math.random() * matches.length)];
|
|
171
|
+
}
|
|
143
172
|
/** `Factory::new()`. */
|
|
144
173
|
static new() {
|
|
145
174
|
return new this();
|
|
@@ -157,52 +186,89 @@ export class Factory {
|
|
|
157
186
|
attrs = { ...attrs, ...patch };
|
|
158
187
|
}
|
|
159
188
|
for (const spec of this.#for) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
189
|
+
let parentModel;
|
|
190
|
+
if (spec.parent instanceof Factory) {
|
|
191
|
+
let created = this.#forParents.get(spec);
|
|
192
|
+
if (!created) {
|
|
193
|
+
created = this.#recycledFor(spec.parent.modelClass());
|
|
194
|
+
}
|
|
195
|
+
if (!created) {
|
|
196
|
+
if (this.#recycled.length > 0)
|
|
197
|
+
spec.parent.recycle(this.#recycled);
|
|
198
|
+
created = (await spec.parent.create());
|
|
199
|
+
}
|
|
200
|
+
this.#forParents.set(spec, created);
|
|
201
|
+
parentModel = created;
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
parentModel = spec.parent;
|
|
205
|
+
}
|
|
163
206
|
const ParentCtor = parentModel.constructor;
|
|
164
207
|
const fkName = spec.relationship?.endsWith("_id")
|
|
165
208
|
? spec.relationship
|
|
166
|
-
: spec.relationship
|
|
167
|
-
? `${spec.relationship}_id`
|
|
168
|
-
: `${singularTable(ParentCtor.table)}_id`;
|
|
209
|
+
: this.#belongsToKey(ParentCtor, spec.relationship);
|
|
169
210
|
attrs[fkName] = parentModel[ParentCtor.primaryKey];
|
|
170
211
|
}
|
|
171
212
|
return { ...attrs, ...attributes };
|
|
172
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Foreign key for `for(parent)`: the child's `belongsTo` relation named after
|
|
216
|
+
* the parent class (a `Team` parent looks for `team()`), else `team_id`.
|
|
217
|
+
*/
|
|
218
|
+
#belongsToKey(Parent, relationship) {
|
|
219
|
+
const camel = relationship ?? Parent.name.charAt(0).toLowerCase() + Parent.name.slice(1);
|
|
220
|
+
try {
|
|
221
|
+
const rel = new (this.model())().related(camel);
|
|
222
|
+
if (typeof rel.getForeignKeyName === "function")
|
|
223
|
+
return rel.getForeignKeyName();
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// no such relation on the child model
|
|
227
|
+
}
|
|
228
|
+
const snake = camel.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
|
|
229
|
+
return `${snake}_id`;
|
|
230
|
+
}
|
|
173
231
|
async #createRelated(parent) {
|
|
174
232
|
const ParentCtor = parent.constructor;
|
|
175
233
|
const parentId = parent[ParentCtor.primaryKey];
|
|
234
|
+
const idsOf = (list) => list.map((m) => m[m.constructor.primaryKey]);
|
|
176
235
|
for (const spec of this.#has) {
|
|
236
|
+
if (this.#recycled.length > 0)
|
|
237
|
+
spec.factory.recycle(this.#recycled);
|
|
177
238
|
const RelatedCtor = spec.factory.modelClass();
|
|
178
239
|
const relName = spec.relationship ?? guessRelationName(ParentCtor, RelatedCtor);
|
|
179
240
|
let fk = `${singularTable(ParentCtor.table)}_id`;
|
|
180
|
-
let
|
|
241
|
+
let fixed = null;
|
|
242
|
+
let rel = null;
|
|
181
243
|
try {
|
|
182
|
-
|
|
183
|
-
if (typeof rel.attach === "function") {
|
|
184
|
-
useAttach = true;
|
|
185
|
-
}
|
|
186
|
-
else if (typeof rel.getForeignKeyName === "function") {
|
|
187
|
-
fk = rel.getForeignKeyName();
|
|
188
|
-
}
|
|
244
|
+
rel = parent.related(relName);
|
|
189
245
|
}
|
|
190
246
|
catch {
|
|
191
|
-
//
|
|
247
|
+
// fall back to the conventional foreign key
|
|
192
248
|
}
|
|
193
|
-
if (
|
|
194
|
-
const
|
|
195
|
-
const list = Array.isArray(
|
|
196
|
-
|
|
197
|
-
const ids = list.map((m) => m[m.constructor.primaryKey]);
|
|
198
|
-
await rel.attach(ids);
|
|
249
|
+
if (rel && typeof rel.attach === "function") {
|
|
250
|
+
const created = await spec.factory.create();
|
|
251
|
+
const list = (Array.isArray(created) ? created : [created]);
|
|
252
|
+
await rel.attach(idsOf(list));
|
|
199
253
|
continue;
|
|
200
254
|
}
|
|
201
|
-
|
|
255
|
+
if (rel && typeof rel.getTypeColumn === "function") {
|
|
256
|
+
// morphOne / morphMany: set both the id and the type column.
|
|
257
|
+
fixed = {
|
|
258
|
+
[rel.getIdColumn()]: parentId,
|
|
259
|
+
[rel.getTypeColumn()]: rel.getMorphType(),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
else if (rel && typeof rel.getForeignKeyName === "function") {
|
|
263
|
+
fk = rel.getForeignKeyName();
|
|
264
|
+
}
|
|
265
|
+
await spec.factory.state(fixed ?? { [fk]: parentId }).create();
|
|
202
266
|
}
|
|
203
267
|
for (const spec of this.#hasAttached) {
|
|
204
268
|
let models;
|
|
205
269
|
if (spec.related instanceof Factory) {
|
|
270
|
+
if (this.#recycled.length > 0)
|
|
271
|
+
spec.related.recycle(this.#recycled);
|
|
206
272
|
const created = await spec.related.create();
|
|
207
273
|
models = Array.isArray(created) ? created : [created];
|
|
208
274
|
}
|
|
@@ -217,17 +283,33 @@ export class Factory {
|
|
|
217
283
|
const RelatedCtor = models[0].constructor;
|
|
218
284
|
const relName = spec.relationship ?? guessRelationName(ParentCtor, RelatedCtor);
|
|
219
285
|
const rel = parent.related(relName);
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
286
|
+
const pivot = spec.pivot;
|
|
287
|
+
if (typeof pivot === "function") {
|
|
288
|
+
// Per-model pivot attributes.
|
|
289
|
+
const map = {};
|
|
290
|
+
for (const m of models)
|
|
291
|
+
map[String(idsOf([m])[0])] = pivot(m);
|
|
292
|
+
await rel.attach(map);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
await rel.attach(idsOf(models), pivot ?? {});
|
|
296
|
+
}
|
|
223
297
|
}
|
|
224
298
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
299
|
+
#begin() {
|
|
300
|
+
if (!this.#configured) {
|
|
301
|
+
this.#configured = true;
|
|
302
|
+
this.configure();
|
|
303
|
+
}
|
|
304
|
+
const out = { n: this.#count, many: this.#many };
|
|
229
305
|
this.#count = 1;
|
|
230
306
|
this.#many = false;
|
|
307
|
+
this.#forParents.clear();
|
|
308
|
+
return out;
|
|
309
|
+
}
|
|
310
|
+
/** Build an in-memory model instance (not persisted). */
|
|
311
|
+
async make(attributes = {}) {
|
|
312
|
+
const { n, many } = this.#begin();
|
|
231
313
|
const ModelClass = this.model();
|
|
232
314
|
const items = [];
|
|
233
315
|
for (let i = 0; i < n; i++) {
|
|
@@ -242,18 +324,16 @@ export class Factory {
|
|
|
242
324
|
}
|
|
243
325
|
/** Persist model(s) via `Model.forceCreate` (factories set guarded columns too). */
|
|
244
326
|
async create(attributes = {}) {
|
|
245
|
-
const n = this.#
|
|
246
|
-
const many = this.#many;
|
|
247
|
-
this.#count = 1;
|
|
248
|
-
this.#many = false;
|
|
327
|
+
const { n, many } = this.#begin();
|
|
249
328
|
const ModelClass = this.model();
|
|
250
329
|
const items = [];
|
|
251
330
|
for (let i = 0; i < n; i++) {
|
|
252
331
|
const base = await this.#resolveAttributes(attributes);
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
332
|
+
// `afterMaking` runs on the unsaved model, then it is persisted.
|
|
333
|
+
const model = (await ModelClass.forceCreate(base, async (unsaved) => {
|
|
334
|
+
for (const cb of this.#afterMaking)
|
|
335
|
+
await cb(unsaved);
|
|
336
|
+
}));
|
|
257
337
|
for (const cb of this.#afterCreating) {
|
|
258
338
|
await cb(model);
|
|
259
339
|
}
|
|
@@ -262,4 +342,30 @@ export class Factory {
|
|
|
262
342
|
}
|
|
263
343
|
return (many ? items : items[0]);
|
|
264
344
|
}
|
|
345
|
+
/** `makeOne`. */
|
|
346
|
+
async makeOne(attributes = {}) {
|
|
347
|
+
return (await this.make(attributes));
|
|
348
|
+
}
|
|
349
|
+
/** `createOne`. */
|
|
350
|
+
async createOne(attributes = {}) {
|
|
351
|
+
return (await this.create(attributes));
|
|
352
|
+
}
|
|
353
|
+
/** `createMany(3)` or `createMany([{ name: "a" }, { name: "b" }])`. */
|
|
354
|
+
async createMany(records) {
|
|
355
|
+
if (typeof records === "number") {
|
|
356
|
+
return (await this.count(records).create());
|
|
357
|
+
}
|
|
358
|
+
const out = [];
|
|
359
|
+
for (const attrs of records)
|
|
360
|
+
out.push((await this.createOne(attrs)));
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
/** `createQuietly` — no model events. */
|
|
364
|
+
async createQuietly(attributes = {}) {
|
|
365
|
+
return this.model().withoutEvents(() => this.create(attributes));
|
|
366
|
+
}
|
|
367
|
+
/** `createManyQuietly`. */
|
|
368
|
+
async createManyQuietly(records) {
|
|
369
|
+
return this.model().withoutEvents(() => this.createMany(records));
|
|
370
|
+
}
|
|
265
371
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Model, ModelQuery, HasMany, HasManyThrough, HasOne, HasOneThrough, BelongsTo, BelongsToMany, MorphToMany, MorphMany, MorphOne, MorphTo, morphMap, clearMorphMap, eagerLoadModels, eagerLoadAggregates, ModelNotFoundException, LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, resetModelStrictnessForTests, isModelCtor, Attribute, AsCollection, AsFluent, AsEnumCollection, resetModelEventsForTests, type ModelClass, type RelationFactory, type RelationsMap, type CastType, type CastDefinition, type EnumCast, type ModelEventName, type ModelEventListener, type ModelObserver, type ModelQueryOptions, type EagerAggregateSpec, type MorphToManyOptions, resolveConnection, } from "./model.js";
|
|
1
|
+
export { Model, ModelQuery, HasMany, HasManyThrough, HasOne, HasOneThrough, BelongsTo, BelongsToMany, MorphToMany, MorphedByMany, MorphMany, MorphOne, MorphTo, morphMap, enforceMorphMap, requireMorphMap, getMorphedModel, clearMorphMap, eagerLoadModels, eagerLoadAggregates, ModelNotFoundException, LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, resetModelStrictnessForTests, isModelCtor, Attribute, AsCollection, AsFluent, AsEnumCollection, resetModelEventsForTests, type ModelClass, type RelationFactory, type RelationsMap, type CastType, type CastDefinition, type EnumCast, type ModelEventName, type ModelEventListener, type ModelObserver, type ModelQueryOptions, type EagerAggregateSpec, type MorphToManyOptions, resolveConnection, } from "./model.js";
|
|
2
2
|
export type { GlobalScopeCallback } from "./scopes.js";
|
|
3
3
|
export { clearGlobalScopes } from "./scopes.js";
|
|
4
4
|
export { UseBuilder, resolveModelBuilder, type ModelBuilderConstructor, } from "./decorators.js";
|
|
@@ -8,3 +8,23 @@ export { Factory, fake, type FactoryAttributes, type FactoryState, type FactoryS
|
|
|
8
8
|
export { Pivot } from "./pivot.js";
|
|
9
9
|
export { LengthAwarePaginator } from "@bunyad/database";
|
|
10
10
|
export { Collection, collect } from "@bunyad/common";
|
|
11
|
+
export type { AggregateRelations } from "./model-query.js";
|
|
12
|
+
/**
|
|
13
|
+
* Type-level switch for the whole ORM, declared here (the package entry) so an app can augment it.
|
|
14
|
+
*
|
|
15
|
+
* Relation and column names are checked at compile time by default: `User.with("psots")` and
|
|
16
|
+
* `User.where("emial", x)` are errors. To accept any string again, add this to a `.d.ts` file in your app:
|
|
17
|
+
*
|
|
18
|
+
* ```ts
|
|
19
|
+
* declare module "@bunyad/orm" {
|
|
20
|
+
* interface OrmTypeOptions {
|
|
21
|
+
* strictNames: false;
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export interface OrmTypeOptions {
|
|
27
|
+
}
|
|
28
|
+
export type { ColumnHint, ColumnNames, RelationHint, RelationNames, } from "./typed-names.js";
|
|
29
|
+
export { unsafeName } from "./typed-names.js";
|
|
30
|
+
export { checkModelSchema, generateModelTypes, modelColumnsInterface, tsTypeForColumn, type SchemaIssue } from "./schema-types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { Model, ModelQuery, HasMany, HasManyThrough, HasOne, HasOneThrough, BelongsTo, BelongsToMany, MorphToMany, MorphMany, MorphOne, MorphTo, morphMap, clearMorphMap, eagerLoadModels, eagerLoadAggregates, ModelNotFoundException, LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, resetModelStrictnessForTests, isModelCtor, Attribute, AsCollection, AsFluent, AsEnumCollection, resetModelEventsForTests, resolveConnection, } from "./model.js";
|
|
1
|
+
export { Model, ModelQuery, HasMany, HasManyThrough, HasOne, HasOneThrough, BelongsTo, BelongsToMany, MorphToMany, MorphedByMany, MorphMany, MorphOne, MorphTo, morphMap, enforceMorphMap, requireMorphMap, getMorphedModel, clearMorphMap, eagerLoadModels, eagerLoadAggregates, ModelNotFoundException, LazyLoadingViolationException, MassAssignmentException, MissingAttributeException, resetModelStrictnessForTests, isModelCtor, Attribute, AsCollection, AsFluent, AsEnumCollection, resetModelEventsForTests, resolveConnection, } from "./model.js";
|
|
2
2
|
export { clearGlobalScopes } from "./scopes.js";
|
|
3
3
|
export { UseBuilder, resolveModelBuilder, } from "./decorators.js";
|
|
4
4
|
export { HasUuids, HasUlids, HasFactory, SoftDeletes, Prunable, MassPrunable, prune, pruneAll, prunableModelClasses, massPrunableModelClasses, clearPrunableRegistries, uuid7, ulid, } from "./concerns.js";
|
|
@@ -7,3 +7,5 @@ export { Factory, fake, } from "./factory.js";
|
|
|
7
7
|
export { Pivot } from "./pivot.js";
|
|
8
8
|
export { LengthAwarePaginator } from "@bunyad/database";
|
|
9
9
|
export { Collection, collect } from "@bunyad/common";
|
|
10
|
+
export { unsafeName } from "./typed-names.js";
|
|
11
|
+
export { checkModelSchema, generateModelTypes, modelColumnsInterface, tsTypeForColumn } from "./schema-types.js";
|
package/dist/model-events.d.ts
CHANGED
|
@@ -10,6 +10,9 @@ export type ModelEventListener<T extends Model = Model> = (model: T) => void | b
|
|
|
10
10
|
/** Observer object with optional methods named after events. */
|
|
11
11
|
export type ModelObserver<T extends Model = Model> = {
|
|
12
12
|
[K in ModelEventName]?: (model: T) => void | boolean | Promise<void | boolean>;
|
|
13
|
+
} & {
|
|
14
|
+
/** Dispatch the after-events (`created`, `saved`, …) once the transaction commits. */
|
|
15
|
+
afterCommit?: boolean;
|
|
13
16
|
};
|
|
14
17
|
type ModelCtor = abstract new (...args: never[]) => Model;
|
|
15
18
|
/** Whether events are currently suppressed for this model class. */
|
|
@@ -34,8 +37,11 @@ export declare function fireModelEvent(model: Model, event: ModelEventName): boo
|
|
|
34
37
|
/** True when at least one listener is registered for this event. */
|
|
35
38
|
export declare function hasModelEventListeners(ctor: ModelCtor, event: ModelEventName): boolean;
|
|
36
39
|
export declare function hasAnyModelEventListeners(ctor: ModelCtor): boolean;
|
|
37
|
-
/**
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Run `callback` with model events disabled for `ctor`, or for every model
|
|
42
|
+
* when `all` is true (`Model.withoutEvents(...)` on the base class).
|
|
43
|
+
*/
|
|
44
|
+
export declare function withoutModelEvents<T>(ctor: ModelCtor, callback: () => T | Promise<T>, all?: boolean): T | Promise<T>;
|
|
39
45
|
/** Register an observer instance or class (`Model::observe`). */
|
|
40
46
|
export declare function observeModel<T extends Model>(ctor: abstract new (...args: never[]) => T, observer: ModelObserver<T> | (new () => ModelObserver<T>) | Array<ModelObserver<T> | (new () => ModelObserver<T>)>): void;
|
|
41
47
|
/** Test helper — clear boot state and listeners for a model class. */
|
package/dist/model-events.js
CHANGED
|
@@ -18,6 +18,8 @@ export const MODEL_EVENT_NAMES = [
|
|
|
18
18
|
const booted = new WeakSet();
|
|
19
19
|
const listeners = new WeakMap();
|
|
20
20
|
const eventsDisabled = new WeakMap();
|
|
21
|
+
/** `Model.withoutEvents()` called on the base class: every model is muted (seeders, imports). */
|
|
22
|
+
let globalEventsDisabled = 0;
|
|
21
23
|
function listenerMap(ctor) {
|
|
22
24
|
let map = listeners.get(ctor);
|
|
23
25
|
if (!map) {
|
|
@@ -28,7 +30,7 @@ function listenerMap(ctor) {
|
|
|
28
30
|
}
|
|
29
31
|
/** Whether events are currently suppressed for this model class. */
|
|
30
32
|
export function modelEventsDisabled(ctor) {
|
|
31
|
-
return (eventsDisabled.get(ctor) ?? 0) > 0;
|
|
33
|
+
return globalEventsDisabled > 0 || (eventsDisabled.get(ctor) ?? 0) > 0;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Ensure `boot` / `booted` have run once for this model class.
|
|
@@ -92,10 +94,20 @@ export function hasAnyModelEventListeners(ctor) {
|
|
|
92
94
|
}
|
|
93
95
|
return false;
|
|
94
96
|
}
|
|
95
|
-
/**
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Run `callback` with model events disabled for `ctor`, or for every model
|
|
99
|
+
* when `all` is true (`Model.withoutEvents(...)` on the base class).
|
|
100
|
+
*/
|
|
101
|
+
export function withoutModelEvents(ctor, callback, all = false) {
|
|
102
|
+
if (all)
|
|
103
|
+
globalEventsDisabled++;
|
|
104
|
+
else
|
|
105
|
+
eventsDisabled.set(ctor, (eventsDisabled.get(ctor) ?? 0) + 1);
|
|
98
106
|
const finish = () => {
|
|
107
|
+
if (all) {
|
|
108
|
+
globalEventsDisabled = Math.max(0, globalEventsDisabled - 1);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
99
111
|
const depth = (eventsDisabled.get(ctor) ?? 1) - 1;
|
|
100
112
|
if (depth <= 0)
|
|
101
113
|
eventsDisabled.delete(ctor);
|
|
@@ -125,6 +137,17 @@ export function observeModel(ctor, observer) {
|
|
|
125
137
|
for (const event of MODEL_EVENT_NAMES) {
|
|
126
138
|
const method = instance[event];
|
|
127
139
|
if (typeof method === "function") {
|
|
140
|
+
const isBefore = event.endsWith("ing") && event !== "replicating";
|
|
141
|
+
if (instance.afterCommit === true && !isBefore) {
|
|
142
|
+
// Run once the outermost
|
|
143
|
+
// transaction commits (immediately when there is none); dropped on rollback.
|
|
144
|
+
registerModelEvent(ctor, event, (model) => {
|
|
145
|
+
model.getConnection().afterCommit(async () => {
|
|
146
|
+
await method.call(instance, model);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
128
151
|
registerModelEvent(ctor, event, (model) => method.call(instance, model));
|
|
129
152
|
}
|
|
130
153
|
}
|
package/dist/model-query.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { DateInput, QueryBuilder } from "@bunyad/database";
|
|
|
5
5
|
import { CursorPaginator, LengthAwarePaginator, Paginator } from "@bunyad/database";
|
|
6
6
|
import { Collection, LazyCollection } from "@bunyad/common";
|
|
7
7
|
import { OrmCollection } from "./orm-collection.js";
|
|
8
|
+
import type { ColumnHint, RelationHint, WithMap } from "./typed-names.js";
|
|
8
9
|
import { type CastDefinition } from "./casts.js";
|
|
9
10
|
import { Model, type ModelClass, type ModelQueryOptions } from "./model.js";
|
|
10
11
|
/**
|
|
@@ -14,6 +15,13 @@ import { Model, type ModelClass, type ModelQueryOptions } from "./model.js";
|
|
|
14
15
|
export declare const MODEL_QUERY_TARGET: unique symbol;
|
|
15
16
|
/** Unwrap a local-scope Proxy to the underlying `ModelQuery` instance. */
|
|
16
17
|
export declare function unwrapModelQuery<Q extends ModelQuery>(query: Q): Q;
|
|
18
|
+
/** Shared empty array — copy-on-write before mutate (ModelQuery ctor hot path). */
|
|
19
|
+
/** Relations accepted by `withCount` / `withSum` / …: names, lists, or constraint maps. */
|
|
20
|
+
export type AggregateRelations = string | AggregateRelations[] | Record<string, true | string | {
|
|
21
|
+
as?: string;
|
|
22
|
+
} | ((query: ModelQuery) => void)>;
|
|
23
|
+
/** A subquery: a query builder, a model query, or a closure that configures a builder. */
|
|
24
|
+
export type SubQuery = QueryBuilder | ModelQuery<any, any> | ((q: QueryBuilder) => void);
|
|
17
25
|
export declare class ModelQuery<T extends Model = Model, TResult extends "model" | "row" = "model"> {
|
|
18
26
|
#private;
|
|
19
27
|
private model;
|
|
@@ -38,15 +46,15 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
38
46
|
/** `withCasts` — merge query-time casts for hydration. */
|
|
39
47
|
withCasts(casts: Record<string, CastDefinition>): this;
|
|
40
48
|
/** `has`. */
|
|
41
|
-
has(relation:
|
|
49
|
+
has(relation: RelationHint<T>): this;
|
|
42
50
|
/** `doesntHave`. */
|
|
43
|
-
doesntHave(relation:
|
|
51
|
+
doesntHave(relation: RelationHint<T>): this;
|
|
44
52
|
/** `whereHas`. */
|
|
45
|
-
whereHas(relation:
|
|
53
|
+
whereHas(relation: RelationHint<T>, callback?: (query: ModelQuery) => void): this;
|
|
46
54
|
/** `orWhereHas`. */
|
|
47
|
-
orWhereHas(relation:
|
|
55
|
+
orWhereHas(relation: RelationHint<T>, callback?: (query: ModelQuery) => void): this;
|
|
48
56
|
/** `whereDoesntHave`. */
|
|
49
|
-
whereDoesntHave(relation:
|
|
57
|
+
whereDoesntHave(relation: RelationHint<T>, callback?: (query: ModelQuery) => void): this;
|
|
50
58
|
/** `orWhereDoesntHave`. */
|
|
51
59
|
orWhereDoesntHave(relation: string, callback?: (query: ModelQuery) => void): this;
|
|
52
60
|
/**
|
|
@@ -93,7 +101,7 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
93
101
|
/**
|
|
94
102
|
* `withWhereHas($relation, $callback?)` — whereHas + eager with same constraint.
|
|
95
103
|
*/
|
|
96
|
-
withWhereHas(relation:
|
|
104
|
+
withWhereHas(relation: RelationHint<T>, callback?: (query: ModelQuery) => void): this;
|
|
97
105
|
/** `whereAny` (forwarded to query builder). */
|
|
98
106
|
whereAny(columns: string[], opOrValue: unknown, value?: unknown): this;
|
|
99
107
|
orWhereAny(columns: string[], opOrValue: unknown, value?: unknown): this;
|
|
@@ -111,7 +119,11 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
111
119
|
whereUlid(column: string, value: string): this;
|
|
112
120
|
/** `whereNot(closure)` nested group. */
|
|
113
121
|
whereNot(callback: (query: ModelQuery<T>) => void): this;
|
|
122
|
+
whereNot(column: ColumnHint<T>, value: unknown): this;
|
|
123
|
+
whereNot(column: ColumnHint<T>, op: string, value: unknown): this;
|
|
114
124
|
orWhereNot(callback: (query: ModelQuery<T>) => void): this;
|
|
125
|
+
orWhereNot(column: ColumnHint<T>, value: unknown): this;
|
|
126
|
+
orWhereNot(column: ColumnHint<T>, op: string, value: unknown): this;
|
|
115
127
|
/** `firstWhere`. */
|
|
116
128
|
firstWhere(column: string, value: unknown): T | null | Promise<T | null>;
|
|
117
129
|
firstWhere(column: string, op: string, value: unknown): T | null | Promise<T | null>;
|
|
@@ -127,19 +139,38 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
127
139
|
valueOrFail(column: string): Promise<unknown>;
|
|
128
140
|
/** `soleValue`. */
|
|
129
141
|
soleValue(column: string): Promise<unknown>;
|
|
130
|
-
/**
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
withSum(relation
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
/**
|
|
143
|
+
* `withCount` — `'posts'`, `'posts as post_total'`, a list, or a map such as
|
|
144
|
+
* `{ "payments as paid": (q) => q.where("status", "paid") }`.
|
|
145
|
+
*/
|
|
146
|
+
withCount(...relations: AggregateRelations[]): this;
|
|
147
|
+
/** `withSum($relation, $column)` — relation may be a constrained map. */
|
|
148
|
+
withSum(relation: AggregateRelations, column: string): this;
|
|
149
|
+
withAvg(relation: AggregateRelations, column: string): this;
|
|
150
|
+
withMin(relation: AggregateRelations, column: string): this;
|
|
151
|
+
withMax(relation: AggregateRelations, column: string): this;
|
|
139
152
|
/** `withExists($relation)`. */
|
|
140
|
-
withExists(...relations:
|
|
153
|
+
withExists(...relations: AggregateRelations[]): this;
|
|
141
154
|
/** `withAggregate($relation, $column, $function)`. */
|
|
142
|
-
withAggregate(relation:
|
|
155
|
+
withAggregate(relation: AggregateRelations, column: string, fn: "count" | "sum" | "avg" | "min" | "max"): this;
|
|
156
|
+
/** `selectSub($query, $as)` — add a subquery as a selected column. */
|
|
157
|
+
selectSub(query: SubQuery, as: string): this;
|
|
158
|
+
/** `whereExists($query)` and its `or` / `not` variants. */
|
|
159
|
+
whereExists(query: SubQuery): this;
|
|
160
|
+
orWhereExists(query: SubQuery): this;
|
|
161
|
+
whereNotExists(query: SubQuery): this;
|
|
162
|
+
orWhereNotExists(query: SubQuery): this;
|
|
163
|
+
/** `union` / `unionAll` — the other query must select the same columns. */
|
|
164
|
+
union(query: SubQuery): this;
|
|
165
|
+
unionAll(query: SubQuery): this;
|
|
166
|
+
/** `joinSub($query, $as, $first, $op, $second)`. */
|
|
167
|
+
joinSub(query: SubQuery, as: string, first: string, op: string, second: string): this;
|
|
168
|
+
leftJoinSub(query: SubQuery, as: string, first: string, op: string, second: string): this;
|
|
169
|
+
rightJoinSub(query: SubQuery, as: string, first: string, op: string, second: string): this;
|
|
170
|
+
/** `crossJoin($table)`. */
|
|
171
|
+
crossJoin(table: string): this;
|
|
172
|
+
/** `groupByRaw($sql, $bindings)`. */
|
|
173
|
+
groupByRaw(sql: string, bindings?: unknown[]): this;
|
|
143
174
|
/** `join` (hydrated models still from the base table). */
|
|
144
175
|
join(table: string, first: string, op: string, second: string): this;
|
|
145
176
|
/** `leftJoin`. */
|
|
@@ -147,18 +178,18 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
147
178
|
/** `rightJoin`. */
|
|
148
179
|
rightJoin(table: string, first: string, op: string, second: string): this;
|
|
149
180
|
where(callback: (query: ModelQuery<T>) => void): this;
|
|
150
|
-
where(column:
|
|
151
|
-
where(column:
|
|
181
|
+
where(column: ColumnHint<T>, value: unknown): this;
|
|
182
|
+
where(column: ColumnHint<T>, op: string, value: unknown): this;
|
|
152
183
|
orWhere(callback: (query: ModelQuery<T>) => void): this;
|
|
153
|
-
orWhere(column:
|
|
154
|
-
orWhere(column:
|
|
155
|
-
whereIn(column:
|
|
184
|
+
orWhere(column: ColumnHint<T>, value: unknown): this;
|
|
185
|
+
orWhere(column: ColumnHint<T>, op: string, value: unknown): this;
|
|
186
|
+
whereIn(column: ColumnHint<T>, values: unknown[]): this;
|
|
156
187
|
orWhereIn(column: string, values: unknown[]): this;
|
|
157
188
|
whereNotIn(column: string, values: unknown[]): this;
|
|
158
189
|
orWhereNotIn(column: string, values: unknown[]): this;
|
|
159
|
-
whereNull(column:
|
|
190
|
+
whereNull(column: ColumnHint<T>): this;
|
|
160
191
|
orWhereNull(column: string): this;
|
|
161
|
-
whereNotNull(column:
|
|
192
|
+
whereNotNull(column: ColumnHint<T>): this;
|
|
162
193
|
orWhereNotNull(column: string): this;
|
|
163
194
|
whereBetween(column: string, values: [unknown, unknown]): this;
|
|
164
195
|
orWhereBetween(column: string, values: [unknown, unknown]): this;
|
|
@@ -184,10 +215,10 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
184
215
|
addSelect(...columns: string[]): this;
|
|
185
216
|
selectRaw(expression: string, bindings?: unknown[]): this;
|
|
186
217
|
distinct(value?: boolean): this;
|
|
187
|
-
orderBy(column:
|
|
188
|
-
orderByDesc(column:
|
|
218
|
+
orderBy(column: ColumnHint<T>, direction?: "asc" | "desc"): this;
|
|
219
|
+
orderByDesc(column: ColumnHint<T>): this;
|
|
189
220
|
orderByRaw(sql: string): this;
|
|
190
|
-
/**
|
|
221
|
+
/** Alias for `orderByRaw`. */
|
|
191
222
|
orderBySql(sql: string): this;
|
|
192
223
|
reorder(column?: string, direction?: "asc" | "desc"): this;
|
|
193
224
|
inRandomOrder(): this;
|
|
@@ -198,6 +229,15 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
198
229
|
havingRaw(sql: string, bindings?: unknown[]): this;
|
|
199
230
|
limit(value: number): this;
|
|
200
231
|
take(value: number): this;
|
|
232
|
+
/**
|
|
233
|
+
* Remove and return this query's limit / offset. Eager loading calls it so a
|
|
234
|
+
* `with({ posts: (q) => q.limit(3) })` limit applies per parent, not to the
|
|
235
|
+
* whole batched query.
|
|
236
|
+
*/
|
|
237
|
+
takePaging(): {
|
|
238
|
+
limit?: number;
|
|
239
|
+
offset?: number;
|
|
240
|
+
};
|
|
201
241
|
offset(value: number): this;
|
|
202
242
|
skip(value: number): this;
|
|
203
243
|
forPage(page: number, perPage?: number): this;
|
|
@@ -205,14 +245,14 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
205
245
|
sharedLock(): this;
|
|
206
246
|
when(condition: unknown, callback: (query: this) => void, defaultCallback?: (query: this) => void): this;
|
|
207
247
|
/**
|
|
208
|
-
* OR `LIKE %term%` across columns (
|
|
248
|
+
* OR `LIKE %term%` across columns (a list `search`).
|
|
209
249
|
* Empty/whitespace term is a no-op.
|
|
210
250
|
*/
|
|
211
251
|
search(columns: string[], term: string): this;
|
|
212
252
|
unless(condition: unknown, callback: (query: this) => void, defaultCallback?: (query: this) => void): this;
|
|
213
253
|
tap(callback: (query: this) => void): this;
|
|
214
254
|
clone(): ModelQuery<T, TResult>;
|
|
215
|
-
with(...relations: Array<
|
|
255
|
+
with(...relations: Array<RelationHint<T> | RelationHint<T>[] | WithMap<T>>): this;
|
|
216
256
|
/** Underlying query builder (fresh compile each call). */
|
|
217
257
|
toBase(): QueryBuilder;
|
|
218
258
|
/** Compiled SQL (bindings via `getBindings()`). */
|
|
@@ -222,6 +262,12 @@ export declare class ModelQuery<T extends Model = Model, TResult extends "model"
|
|
|
222
262
|
getBindings(): unknown[];
|
|
223
263
|
dump(): this;
|
|
224
264
|
dd(): never;
|
|
265
|
+
/**
|
|
266
|
+
* Apply this model's global scopes onto a correlated subquery (`whereHas`, `withCount`, …),
|
|
267
|
+
* qualifying columns with `table` so joined pivot / parent columns never clash.
|
|
268
|
+
* Honors `withoutGlobalScopes()` called inside the user's constraint closure.
|
|
269
|
+
*/
|
|
270
|
+
applyGlobalScopesToSubquery(q: QueryBuilder, table: string): QueryBuilder;
|
|
225
271
|
/**
|
|
226
272
|
* Prefix unqualified columns with `table.` (BelongsTo whereHas → JOIN path).
|
|
227
273
|
*/
|