@rudderjs/orm 1.5.0 → 1.7.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/README.md CHANGED
@@ -195,7 +195,106 @@ await Comment.create({
195
195
 
196
196
  The discriminator stored in `{morphName}Type` defaults to the parent's class name (`'Post'`, `'Video'`). Override per-class with `static morphAlias = 'post'` to decouple persisted values from JS class names — useful for rename-safe storage. Once set and data exists, treat it as immutable. In dev mode (`NODE_ENV !== 'production'`), `morphTo` resolution checks the `types` list for duplicate discriminators and throws if two classes resolve to the same value.
197
197
 
198
- `belongsToMany` and polymorphic v1 limitations: pivot columns are not surfaced on read results (write side only), no `withTimestamps`, no `morphToMany` / `morphedByMany`, no fluent eager-load (`User.with('comments.commentable')`) — drop to the adapter (Prisma `include`) for those. Mutations on the deferred read query (`create`/`update`/`delete`/`insertMany`/`deleteAll`) throw — write through the related model directly.
198
+ `belongsToMany` and polymorphic v1 limitations: pivot columns are not surfaced on read results (write side only), no `withTimestamps`, no fluent eager-load (`User.with('comments.commentable')`) — drop to the adapter (Prisma `include`) for that. Mutations on the deferred read query (`create`/`update`/`delete`/`insertMany`/`deleteAll`) throw — write through the related model directly. `morphToMany` / `morphedByMany` are supported with the same `attach` / `detach` / `sync` accessor as `belongsToMany`, plus discriminator-scoped pivot reads/writes — see the [polymorphic many-to-many guide](https://rudderjs.com/docs/database/models#polymorphic-many-to-many-morphtomany-morphedbymany).
199
+
200
+ ### Filtering by relation predicate — `whereHas` / `whereDoesntHave` / `withWhereHas` / `whereBelongsTo`
201
+
202
+ Filter a query by whether a relation has at least one matching row. The optional callback narrows the relation predicate further — chain plain `where()` calls inside it.
203
+
204
+ ```ts
205
+ // Users with at least one post
206
+ await User.whereHas('posts').get()
207
+
208
+ // Users with at least one published post
209
+ await User.whereHas('posts', q => q.where('published', true)).get()
210
+
211
+ // Inverse — users with zero published posts
212
+ await User.whereDoesntHave('posts', q => q.where('published', true)).get()
213
+
214
+ // Filter AND eager-load under the same constraint (constrained eager-load
215
+ // via the adapter's `withConstrained` when supported, falls back to plain
216
+ // `with(relation)` otherwise — Drizzle today)
217
+ await User.withWhereHas('posts', q => q.where('published', true)).get()
218
+
219
+ // Sugar over `where(fk, parent.id)` — looks up the FK column from the
220
+ // belongsTo declaration. Pass the relation name when the calling class
221
+ // has multiple belongsTo to the same parent.
222
+ await Post.whereBelongsTo(user).get()
223
+ await Comment.whereBelongsTo(post, 'post').get()
224
+ ```
225
+
226
+ Supported relation types: `hasMany`, `hasOne`, `belongsTo`, `belongsToMany`, `morphMany`, `morphOne`, `morphToMany`, `morphedByMany`. **`morphTo` is intentionally not supported** — the related table is dynamic, so a single subquery can't represent it. Filter on the `{morphName}Id` / `{morphName}Type` columns directly when you need that semantic.
227
+
228
+ **Adapter notes:**
229
+
230
+ - **Prisma** uses native `some` / `none` filters for direct relations (`hasMany`/`hasOne`/`belongsTo`) — those relations must be declared in `schema.prisma` with the same name. Polymorphic and pivot relations route through a 2-step lookup (related → pivot → IN list) so they work without a Prisma-declared relation.
231
+ - **Drizzle** uses correlated `EXISTS (...)` / `NOT EXISTS (...)` subqueries. Every related table referenced from a `whereHas` call must be registered via `tables: { ... }` on `drizzle()` config or `DrizzleTableRegistry.register(name, table)`.
232
+ - **`withWhereHas`** uses `withConstrained` when the adapter implements it (Prisma → nested `include: { rel: { where } }`). The Drizzle adapter doesn't yet — `withWhereHas` falls back to plain `with(relation)` there.
233
+ - **Nested `whereHas` inside the constrain callback throws** — recursive predicates are deferred to v2. Filter on flat columns inside the callback for now.
234
+ - **Soft deletes inside the relation predicate** — apply `q.where('deletedAt', null)` explicitly inside the constrain callback when needed.
235
+
236
+ ### Aggregate eager loading — `withCount` / `withSum` / `withMin` / `withMax` / `withAvg` / `withExists`
237
+
238
+ Eager-load aggregates of related rows alongside the parent in a single query. The result is stamped onto each parent under a deterministic alias (`<relation><Verb><Column>`) so admin tables, dashboards, and any list page can render counts / sums next to each row without N+1.
239
+
240
+ ```ts
241
+ // Counts: stamps user.postsCount on each row
242
+ await User.query().withCount('posts').get()
243
+
244
+ // Sum / min / max / avg of a related column — stamps postsSumViews etc.
245
+ await User.query().withSum('posts', 'views').get()
246
+ await Login.query().withMax('sessions', 'createdAt').get()
247
+
248
+ // Boolean — stamps subscriptionExists (true/false)
249
+ await User.query().withExists('subscription').get()
250
+
251
+ // Multiple at once
252
+ await User.query()
253
+ .withCount('posts')
254
+ .withSum('orders', 'total')
255
+ .paginate(1)
256
+ ```
257
+
258
+ **Constraint callbacks (map form)** — narrow what counts as a "matching" row, optionally aliasing the result key:
259
+
260
+ ```ts
261
+ await User.query()
262
+ .withCount({ posts: q => q.where('published', true).as('publishedPosts') })
263
+ .get()
264
+ // → user.publishedPostsCount
265
+
266
+ await User.query()
267
+ .withSum({
268
+ orders: { column: 'total', constraint: q => q.where('status', 'paid') },
269
+ })
270
+ .get()
271
+ // → user.ordersSumTotal
272
+ ```
273
+
274
+ **Per-instance variants** — `loadCount` / `loadExists` / `loadSum` / `loadMin` / `loadMax` / `loadAvg` mutate a single instance in place. Use these when you've already fetched one parent and need the aggregate on demand. For batched loads on a list, prefer `Model.query().withCount(...)` on the parent query.
275
+
276
+ ```ts
277
+ const user = await User.find(1)
278
+ await user!.loadCount('posts')
279
+ console.log(user!.postsCount)
280
+ ```
281
+
282
+ **`loadMissing(...names)`** — eager-load each named relation onto the instance only when the property is currently `null` / `undefined`. Skips relations that are already populated.
283
+
284
+ ```ts
285
+ const user = await User.query().with('profile').first()
286
+ // profile is already populated; only `posts` issues a query
287
+ await user!.loadMissing('profile', 'posts')
288
+ ```
289
+
290
+ **Notes:**
291
+
292
+ - Aggregate columns are enumerable own-properties — they appear in `JSON.stringify(row)`, `Object.entries(row)`, and `{ ...row }` spreads. They're tagged via a Symbol so `model.save()` strips them out before writing back to the DB.
293
+ - **`withCount` on `belongsTo` throws** (every parent matches exactly one row, so the count is always 0 or 1). Use `withExists('relation')` to test presence, or query the inverse `hasMany` side.
294
+ - **`withCount` on `morphTo` throws** — the related table is dynamic. Aggregate per-target by querying each target class separately.
295
+ - Results are typed `unknown` at the property-access site — cast at the call site (`(user as { postsCount: number }).postsCount`) since the QB type doesn't track the injected aliases. The instance load path doesn't need a cast at the access site.
296
+ - **Soft deletes** on the related model are applied automatically — the adapter ANDs `deleted_at IS NULL` into the aggregate subquery.
297
+ - **Adapter behavior**: Prisma uses `_count.select` for direct count/exists (round-trip-saving) and a second-batch `groupBy` for polymorphic / pivot / numeric aggregates. Drizzle emits one correlated subselect per aggregate in the SELECT list, joining through the pivot table when present.
199
298
 
200
299
  ---
201
300
 
@@ -599,6 +698,58 @@ await Article.query().scope('byAuthor', userId).get()
599
698
 
600
699
  ---
601
700
 
701
+ ## Dirty Tracking
702
+
703
+ Every Model instance keeps a snapshot of its attributes as of the last
704
+ `hydrate()` / `save()` / `refresh()`. Use it to inspect what changed before
705
+ or after persistence.
706
+
707
+ ```ts
708
+ const user = await User.find(1) // hydrated → not dirty
709
+ user.email = 'new@x.com'
710
+ user.isDirty() // → true
711
+ user.isDirty('email') // → true
712
+ user.isClean('name') // → true
713
+ user.getDirty() // → { email: 'new@x.com' }
714
+ user.getOriginal('email') // → 'old@x.com'
715
+
716
+ await user.save()
717
+ user.isDirty() // → false (baseline reset)
718
+ user.wasChanged() // → true
719
+ user.wasChanged('email') // → true
720
+ user.getChanges() // → { email: 'new@x.com', updatedAt: ... }
721
+ ```
722
+
723
+ | Method | Returns |
724
+ |---|---|
725
+ | `isDirty(key?)` | true when any (or the named) attribute has changed since the last save / load / refresh. |
726
+ | `isClean(key?)` | inverse of `isDirty`. |
727
+ | `wasChanged(key?)` | true when the most recent `save()` actually persisted a change to that attribute. Stays true until the next save / refresh. |
728
+ | `getOriginal(key?)` | snapshot value(s) as of the last save / load / refresh. With a key, that single value; without, a full copy of the snapshot. |
729
+ | `getChanges()` | diff of attributes that changed during the most recent `save()`. |
730
+ | `getDirty()` | diff of attributes currently dirty (unsaved). |
731
+
732
+ **Equality semantics.** Primitives use `===`. Dates compare by `getTime()`.
733
+ Plain objects and arrays (typically `json` / `array` cast columns) compare
734
+ by `JSON.stringify` — key-order sensitive, so `{ a: 1, b: 2 }` and
735
+ `{ b: 2, a: 1 }` are considered different. This matches Eloquent's posture.
736
+
737
+ **`refresh()` discards pending writes.** A `refresh()` re-reads the row,
738
+ re-baselines `getOriginal()`, and clears `getChanges()`. Eloquent retains
739
+ `wasChanged` past a refresh; we don't — refresh is "throw away pending
740
+ state, re-read from DB."
741
+
742
+ **`increment()` / `decrement()` re-baseline.** After an instance counter
743
+ update, `isDirty('viewCount')` is `false` — the new value becomes the
744
+ baseline. Counter updates are pure data-plane and intentionally don't
745
+ fire observers (see `static increment` notes); dirty tracking matches.
746
+
747
+ **`replicate()` clones are unsaved.** A replicated instance has values
748
+ on it but an empty `getOriginal()`, so `isDirty()` is `true` until the
749
+ clone is saved.
750
+
751
+ ---
752
+
602
753
  ## Soft Deletes
603
754
 
604
755
  ```ts
@@ -617,6 +768,52 @@ Post.query().onlyTrashed().get() // only soft-deleted
617
768
 
618
769
  ---
619
770
 
771
+ ## Pruning
772
+
773
+ Models can opt into `pnpm rudder model:prune` by declaring `static prunable()`. The runner walks the registered models and deletes everything the query returns, in chunks. Two modes:
774
+
775
+ ```ts
776
+ import { Model } from '@rudderjs/orm'
777
+
778
+ // Per-instance — observers fire, soft-deletes honored
779
+ class Session extends Model {
780
+ static override table = 'sessions'
781
+ static prunable() { return this.where('expiresAt', '<', new Date()) }
782
+ static pruning(s: Session) { /* optional pre-delete hook */ }
783
+ }
784
+
785
+ // Bulk — single deleteAll() per chunk; no observers, no pruning() hook,
786
+ // soft-deletes bypassed (mirrors the deleteAll() primitive)
787
+ class FailedJob extends Model {
788
+ static override table = 'failed_jobs'
789
+ static override pruneMode = 'mass' as const
790
+ static prunable() { return this.where('failedAt', '<', new Date(Date.now() - 7 * 86_400_000)) }
791
+ }
792
+ ```
793
+
794
+ Run from the CLI:
795
+
796
+ ```bash
797
+ pnpm rudder model:prune # prune everything
798
+ pnpm rudder model:prune --pretend # dry-run; runs count() only
799
+ pnpm rudder model:prune --model=Session,FailedJob
800
+ pnpm rudder model:prune --except=AuditLog
801
+ pnpm rudder model:prune --chunk=500
802
+ ```
803
+
804
+ Or schedule it from `routes/console.ts`:
805
+
806
+ ```ts
807
+ scheduler.command('model:prune').daily()
808
+ scheduler.command('model:prune --pretend').weeklyOn(0, '09:00')
809
+ ```
810
+
811
+ `Prunable` (default) calls `instance.delete()` per row — observers fire, soft-deletes apply. `MassPrunable` (`pruneMode = 'mass'`) is faster but bypasses both. Index the columns your `prunable()` filter touches; the runner re-queries per chunk because deletions shift the offset. `pruning()` exceptions are logged and the run continues — one bad row doesn't abort the sweep.
812
+
813
+ For programmatic use, `pruneModels({ models, except, chunk, pretend })` returns one `{ model, mode, count }` report per pruned model.
814
+
815
+ ---
816
+
620
817
  ## Observers
621
818
 
622
819
  Register lifecycle hooks on a model to transform data, log events, or cancel operations.
@@ -660,6 +857,34 @@ Article.on('deleting', (id) => { if (id === protectedId) return false })
660
857
  > Use `Model.create()`/`Model.update()`/`Model.delete()` to trigger events.
661
858
  > `Model.query().create()` does NOT fire events.
662
859
 
860
+ ### Quiet Events
861
+
862
+ Persist, delete, or restore an instance without firing observers or
863
+ listeners — useful inside seeders, observer cascades, or any path that
864
+ shouldn't trigger lifecycle work twice.
865
+
866
+ ```ts
867
+ const user = await User.find(1)
868
+ user.email = 'new@x.com'
869
+ await user.saveQuietly() // persists, observers silent
870
+
871
+ await user.deleteQuietly() // removes / soft-deletes silently
872
+
873
+ const trashed = await User.withTrashed().find(2)
874
+ await trashed.restoreQuietly() // clears deletedAt silently
875
+ ```
876
+
877
+ Sugar over `Model.withoutEvents()` — `await ctor.withoutEvents(() => instance.save())`.
878
+
879
+ **Per-class isolation.** Quiet ops mute only the *current* class.
880
+ A `User.saveQuietly()` whose observer cascades into `Comment.delete()`
881
+ still fires `Comment` observers — same posture as Eloquent's
882
+ `saveQuietly`. Wrap the cascade in a broader `withoutEvents` block if
883
+ you need full silence.
884
+
885
+ `instance.restore()` (the non-quiet form) is also available — symmetric
886
+ to `instance.delete()` — and fires `restoring` / `restored` normally.
887
+
663
888
  ---
664
889
 
665
890
  ## toJSON()
@@ -0,0 +1,95 @@
1
+ import type { AggregateFn, AggregateRequest, AggregateJoinShape, WhereClause } from '@rudderjs/contracts';
2
+ import type { Model, RelationDefinition } from './index.js';
3
+ /**
4
+ * Constraint callback for the map form of `withCount` / `withExists`. Receives
5
+ * an {@link AggregateConstraintBuilder} for narrow `where`/`orWhere`/`as`
6
+ * recording. Larger surface (`orderBy`, `limit`, terminals) is intentionally
7
+ * out of scope — the only ambiguous semantics in an aggregate context.
8
+ */
9
+ export type AggregateConstraint = (q: AggregateConstraintBuilder) => AggregateConstraintBuilder;
10
+ /**
11
+ * `where`/`orWhere`/`as` recorder passed to {@link AggregateConstraint}
12
+ * callbacks. Captures clauses for the adapter to AND into the aggregate
13
+ * subquery, plus an optional alias prefix override (`.as('publishedPosts')`).
14
+ */
15
+ export declare class AggregateConstraintBuilder {
16
+ /** @internal */
17
+ readonly _wheres: WhereClause[];
18
+ /** @internal — alias prefix override; falls back to the relation name. */
19
+ _aliasPrefix?: string;
20
+ where(column: string, valueOrOperator: unknown, maybeValue?: unknown): this;
21
+ orWhere(column: string, valueOrOperator: unknown, maybeValue?: unknown): this;
22
+ /**
23
+ * Override the alias prefix used to stamp the aggregate column onto the
24
+ * result row. Default = relation name. The verb suffix (`Count`/`SumX`/etc.)
25
+ * is preserved, so `.as('publishedPosts')` on a `withCount` call yields
26
+ * `publishedPostsCount`.
27
+ *
28
+ * Required when calling the same `withCount` / `withSum` twice on different
29
+ * constraints — distinct aliases prevent the second from clobbering the first.
30
+ */
31
+ as(alias: string): this;
32
+ }
33
+ /** Map-form spec for `withSum` / `withMin` / `withMax` / `withAvg`. */
34
+ export interface AggregateSumSpec {
35
+ column: string;
36
+ constraint?: AggregateConstraint;
37
+ }
38
+ /**
39
+ * Per-instance set of attribute keys that came from an aggregate eager-load,
40
+ * not from the underlying schema. Read by `Model._toData()` to skip these
41
+ * keys on writes and by callers that need to distinguish injected columns.
42
+ *
43
+ * Cross-realm-safe via `Symbol.for(...)` — distinct copies of `@rudderjs/orm`
44
+ * loaded by different module graphs share the same tag.
45
+ */
46
+ export declare const AGGREGATES_SYMBOL: unique symbol;
47
+ /** @internal — read or initialise the aggregate-key set on a Model instance. */
48
+ export declare function aggregateKeysOf(instance: object): Set<string>;
49
+ /**
50
+ * The verb suffix that distinguishes which aggregate is stamped on the parent.
51
+ * Combined with the relation name (or `.as(...)` override) to produce the
52
+ * final alias key — e.g. `posts` + `count` → `postsCount`,
53
+ * `posts` + sum of `views` → `postsSumViews`.
54
+ */
55
+ export declare function aggregateSuffix(fn: AggregateFn, column?: string): string;
56
+ export declare function aggregateAlias(fn: AggregateFn, baseAlias: string, column?: string): string;
57
+ /**
58
+ * Build the {@link AggregateJoinShape} for a relation declared on `Parent`.
59
+ * Mirrors `_buildRelationPredicate` but emits the join-shape subset rather
60
+ * than the full {@link RelationExistencePredicate}. `morphTo` and `belongsTo`
61
+ * are rejected by the caller before reaching this function; everything else
62
+ * routes through.
63
+ */
64
+ export declare function buildAggregateJoinShape(Parent: typeof Model, relation: string, def: Exclude<RelationDefinition, {
65
+ type: 'morphTo' | 'belongsTo';
66
+ }>): AggregateJoinShape;
67
+ type RelationsMapEntry = AggregateConstraint;
68
+ type RelationsMap = Record<string, RelationsMapEntry>;
69
+ /** Public entrypoint: normalize all `withCount(...)` overloads. */
70
+ export declare function normalizeWithCount(Parent: typeof Model, arg: string | readonly string[] | RelationsMap): AggregateRequest[];
71
+ /** Public entrypoint: normalize all `withExists(...)` overloads. */
72
+ export declare function normalizeWithExists(Parent: typeof Model, arg: string | readonly string[]): AggregateRequest[];
73
+ /** Public entrypoint: normalize all `withSum/withMin/withMax/withAvg(...)` overloads. */
74
+ export declare function normalizeWithNumericAggregate(Parent: typeof Model, fn: 'sum' | 'min' | 'max' | 'avg', arg1: string | Record<string, AggregateSumSpec>, arg2?: string): AggregateRequest[];
75
+ /**
76
+ * Implementation of `Model#loadCount` / `loadExists`. Mutates the instance
77
+ * in place; returns it for chaining at the call site.
78
+ */
79
+ export declare function loadCountOrExists(instance: Model, fn: 'count' | 'exists', arg: string | readonly string[] | RelationsMap): Promise<void>;
80
+ /**
81
+ * Implementation of `Model#loadSum` / `loadMin` / `loadMax` / `loadAvg`.
82
+ */
83
+ export declare function loadNumericAggregate(instance: Model, fn: 'sum' | 'min' | 'max' | 'avg', relation: string | Record<string, AggregateSumSpec>, column?: string): Promise<void>;
84
+ /**
85
+ * Implementation of `Model#loadMissing`. Loads each named relation onto the
86
+ * instance only when the property is currently `null` / `undefined`.
87
+ *
88
+ * Always uses `instance.related(name).get()` (the chainable QB form) — for
89
+ * `hasOne` / `belongsTo` / `morphOne` semantics the caller can `[0]` the
90
+ * resulting array if they know the relation is single-valued, or call
91
+ * `loadMissing` only on the *array* relations they care about.
92
+ */
93
+ export declare function loadMissingRelations(instance: Model, names: readonly string[]): Promise<void>;
94
+ export {};
95
+ //# sourceMappingURL=aggregate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"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"}