metal-orm 1.0.67 → 1.0.68

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.67",
3
+ "version": "1.0.68",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -28,9 +28,9 @@ import {
28
28
  TrackedEntity
29
29
  } from './runtime-types.js';
30
30
  import { executeHydrated } from './execute.js';
31
- import { runInTransaction } from './transaction-runner.js';
32
- import { saveGraphInternal, SaveGraphOptions } from './save-graph.js';
33
- import type { SaveGraphInputPayload } from './save-graph-types.js';
31
+ import { runInTransaction } from './transaction-runner.js';
32
+ import { saveGraphInternal, SaveGraphOptions } from './save-graph.js';
33
+ import type { SaveGraphInputPayload } from './save-graph-types.js';
34
34
 
35
35
  /**
36
36
  * Interface for ORM interceptors that allow hooking into the flush lifecycle.
@@ -53,7 +53,7 @@ export interface OrmInterceptor {
53
53
  * Options for creating an OrmSession instance.
54
54
  * @template E - The domain event type
55
55
  */
56
- export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
56
+ export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
57
57
  /** The ORM instance */
58
58
  orm: Orm<E>;
59
59
  /** The database executor */
@@ -63,14 +63,21 @@ export interface OrmSessionOptions<E extends DomainEvent = OrmDomainEvent> {
63
63
  /** Optional interceptors for flush lifecycle hooks */
64
64
  interceptors?: OrmInterceptor[];
65
65
  /** Optional domain event handlers */
66
- domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
67
- }
68
-
69
- /**
70
- * ORM Session that manages entity lifecycle, identity mapping, and database operations.
71
- * @template E - The domain event type
72
- */
73
- export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
66
+ domainEventHandlers?: InitialHandlers<E, OrmSession<E>>;
67
+ }
68
+
69
+ export interface SaveGraphSessionOptions extends SaveGraphOptions {
70
+ /** Wrap the save operation in a transaction (default: true). */
71
+ transactional?: boolean;
72
+ /** Flush after saveGraph when not transactional (default: false). */
73
+ flush?: boolean;
74
+ }
75
+
76
+ /**
77
+ * ORM Session that manages entity lifecycle, identity mapping, and database operations.
78
+ * @template E - The domain event type
79
+ */
80
+ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements EntityContext {
74
81
  /** The ORM instance */
75
82
  readonly orm: Orm<E>;
76
83
  /** The database executor */
@@ -81,10 +88,11 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
81
88
  readonly unitOfWork: UnitOfWork;
82
89
  /** The domain event bus */
83
90
  readonly domainEvents: DomainEventBus<E, OrmSession<E>>;
84
- /** The relation change processor */
85
- readonly relationChanges: RelationChangeProcessor;
86
-
87
- private readonly interceptors: OrmInterceptor[];
91
+ /** The relation change processor */
92
+ readonly relationChanges: RelationChangeProcessor;
93
+
94
+ private readonly interceptors: OrmInterceptor[];
95
+ private saveGraphDefaults?: SaveGraphSessionOptions;
88
96
 
89
97
  /**
90
98
  * Creates a new OrmSession instance.
@@ -236,12 +244,22 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
236
244
  * @param type - The event type
237
245
  * @param handler - The event handler
238
246
  */
239
- registerDomainEventHandler<TType extends E['type']>(
240
- type: TType,
241
- handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
242
- ): void {
243
- this.domainEvents.on(type, handler);
244
- }
247
+ registerDomainEventHandler<TType extends E['type']>(
248
+ type: TType,
249
+ handler: DomainEventHandler<Extract<E, { type: TType }>, OrmSession<E>>
250
+ ): void {
251
+ this.domainEvents.on(type, handler);
252
+ }
253
+
254
+ /**
255
+ * Sets default options applied to all saveGraph calls for this session.
256
+ * Per-call options override these defaults.
257
+ * @param defaults - Default saveGraph options for the session
258
+ */
259
+ withSaveGraphDefaults(defaults: SaveGraphSessionOptions): this {
260
+ this.saveGraphDefaults = { ...defaults };
261
+ return this;
262
+ }
245
263
 
246
264
  /**
247
265
  * Finds an entity by its primary key.
@@ -305,23 +323,85 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
305
323
  * @param options - Graph save options
306
324
  * @returns The root entity instance
307
325
  */
308
- async saveGraph<TCtor extends EntityConstructor<object>>(
309
- entityClass: TCtor,
310
- payload: SaveGraphInputPayload<InstanceType<TCtor>>,
311
- options?: SaveGraphOptions & { transactional?: boolean }
312
- ): Promise<InstanceType<TCtor>>;
313
- async saveGraph<TCtor extends EntityConstructor<object>>(
314
- entityClass: TCtor,
315
- payload: Record<string, unknown>,
316
- options?: SaveGraphOptions & { transactional?: boolean }
317
- ): Promise<InstanceType<TCtor>> {
318
- const { transactional = true, ...graphOptions } = options ?? {};
319
- const execute = () => saveGraphInternal(this, entityClass, payload, graphOptions);
320
- if (!transactional) {
321
- return execute();
322
- }
323
- return this.transaction(() => execute());
324
- }
326
+ async saveGraph<TCtor extends EntityConstructor<object>>(
327
+ entityClass: TCtor,
328
+ payload: SaveGraphInputPayload<InstanceType<TCtor>>,
329
+ options?: SaveGraphSessionOptions
330
+ ): Promise<InstanceType<TCtor>>;
331
+ async saveGraph<TCtor extends EntityConstructor<object>>(
332
+ entityClass: TCtor,
333
+ payload: Record<string, unknown>,
334
+ options?: SaveGraphSessionOptions
335
+ ): Promise<InstanceType<TCtor>> {
336
+ const resolved = this.resolveSaveGraphOptions(options);
337
+ const { transactional = true, flush = false, ...graphOptions } = resolved;
338
+ const execute = () => saveGraphInternal(this, entityClass, payload, graphOptions);
339
+ if (!transactional) {
340
+ const result = await execute();
341
+ if (flush) {
342
+ await this.flush();
343
+ }
344
+ return result;
345
+ }
346
+ return this.transaction(() => execute());
347
+ }
348
+
349
+ /**
350
+ * Saves an entity graph and flushes immediately (defaults to transactional: false).
351
+ * @param entityClass - Root entity constructor
352
+ * @param payload - DTO payload containing column values and nested relations
353
+ * @param options - Graph save options
354
+ * @returns The root entity instance
355
+ */
356
+ async saveGraphAndFlush<TCtor extends EntityConstructor<object>>(
357
+ entityClass: TCtor,
358
+ payload: SaveGraphInputPayload<InstanceType<TCtor>>,
359
+ options?: SaveGraphSessionOptions
360
+ ): Promise<InstanceType<TCtor>> {
361
+ const merged = { ...(options ?? {}), flush: true, transactional: options?.transactional ?? false };
362
+ return this.saveGraph(entityClass, payload, merged);
363
+ }
364
+
365
+ /**
366
+ * Updates an existing entity graph (requires a primary key in the payload).
367
+ * @param entityClass - Root entity constructor
368
+ * @param payload - DTO payload containing column values and nested relations
369
+ * @param options - Graph save options
370
+ * @returns The root entity instance or null if not found
371
+ */
372
+ async updateGraph<TCtor extends EntityConstructor<object>>(
373
+ entityClass: TCtor,
374
+ payload: SaveGraphInputPayload<InstanceType<TCtor>>,
375
+ options?: SaveGraphSessionOptions
376
+ ): Promise<InstanceType<TCtor> | null> {
377
+ const table = getTableDefFromEntity(entityClass);
378
+ if (!table) {
379
+ throw new Error('Entity metadata has not been bootstrapped');
380
+ }
381
+ const primaryKey = findPrimaryKey(table);
382
+ const pkValue = (payload as Record<string, unknown>)[primaryKey];
383
+ if (pkValue === undefined || pkValue === null) {
384
+ throw new Error(`updateGraph requires a primary key value for "${primaryKey}"`);
385
+ }
386
+
387
+ const resolved = this.resolveSaveGraphOptions(options);
388
+ const { transactional = true, flush = false, ...graphOptions } = resolved;
389
+ const execute = async (): Promise<InstanceType<TCtor> | null> => {
390
+ const tracked = this.getEntity(table, pkValue as PrimaryKey) as InstanceType<TCtor> | undefined;
391
+ const existing = tracked ?? await this.find(entityClass, pkValue);
392
+ if (!existing) return null;
393
+ return saveGraphInternal(this, entityClass, payload, graphOptions);
394
+ };
395
+
396
+ if (!transactional) {
397
+ const result = await execute();
398
+ if (result && flush) {
399
+ await this.flush();
400
+ }
401
+ return result;
402
+ }
403
+ return this.transaction(() => execute());
404
+ }
325
405
 
326
406
  /**
327
407
  * Persists an entity (either inserts or updates).
@@ -396,7 +476,7 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
396
476
  * @returns The result of the function
397
477
  * @throws If the transaction fails
398
478
  */
399
- async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
479
+ async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
400
480
  // If the executor can't do transactions, just run and commit once.
401
481
  if (!this.executor.capabilities.transactions) {
402
482
  const result = await fn(this);
@@ -444,16 +524,25 @@ export class OrmSession<E extends DomainEvent = OrmDomainEvent> implements Entit
444
524
  * Gets the hydration context.
445
525
  * @returns The hydration context
446
526
  */
447
- getHydrationContext(): HydrationContext<E> {
448
- return {
449
- identityMap: this.identityMap,
450
- unitOfWork: this.unitOfWork,
451
- domainEvents: this.domainEvents,
452
- relationChanges: this.relationChanges,
453
- entityContext: this
454
- };
455
- }
456
- }
527
+ getHydrationContext(): HydrationContext<E> {
528
+ return {
529
+ identityMap: this.identityMap,
530
+ unitOfWork: this.unitOfWork,
531
+ domainEvents: this.domainEvents,
532
+ relationChanges: this.relationChanges,
533
+ entityContext: this
534
+ };
535
+ }
536
+
537
+ /**
538
+ * Merges session defaults with per-call saveGraph options.
539
+ * @param options - Per-call saveGraph options
540
+ * @returns Combined options with per-call values taking precedence
541
+ */
542
+ private resolveSaveGraphOptions(options?: SaveGraphSessionOptions): SaveGraphSessionOptions {
543
+ return { ...(this.saveGraphDefaults ?? {}), ...(options ?? {}) };
544
+ }
545
+ }
457
546
 
458
547
  const buildRelationChangeEntry = (
459
548
  root: unknown,