metal-orm 1.0.118 → 1.1.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/src/orm/orm.ts CHANGED
@@ -1,25 +1,42 @@
1
- import type { DomainEvent, OrmDomainEvent } from './runtime-types.js';
2
- import type { Dialect } from '../core/dialect/abstract.js';
3
- import type { DbExecutor } from '../core/execution/db-executor.js';
4
- import type { NamingStrategy } from '../codegen/naming-strategy.js';
5
- import { InterceptorPipeline } from './interceptor-pipeline.js';
6
- import { DefaultNamingStrategy } from '../codegen/naming-strategy.js';
7
- import { OrmSession } from './orm-session.js';
1
+ import type { DomainEvent, OrmDomainEvent } from './runtime-types.js';
2
+ import type { Dialect } from '../core/dialect/abstract.js';
3
+ import type { DbExecutor } from '../core/execution/db-executor.js';
4
+ import type { NamingStrategy } from '../codegen/naming-strategy.js';
5
+ import { InterceptorPipeline } from './interceptor-pipeline.js';
6
+ import { DefaultNamingStrategy } from '../codegen/naming-strategy.js';
7
+ import { OrmSession } from './orm-session.js';
8
+ import type { QueryCacheManager } from '../cache/query-cache-manager.js';
9
+ import type { Duration, CacheProvider, CacheStrategy } from '../cache/index.js';
10
+ import { QueryCacheManager as QueryCacheManagerImpl } from '../cache/query-cache-manager.js';
11
+ import { DefaultCacheStrategy } from '../cache/strategies/default-cache-strategy.js';
8
12
 
9
- /**
10
- * Options for creating an ORM instance.
11
- */
12
- export interface OrmOptions {
13
- /** The database dialect */
14
- dialect: Dialect;
15
- /** The database executor factory */
16
- executorFactory: DbExecutorFactory;
17
- /** Optional interceptors pipeline */
18
- interceptors?: InterceptorPipeline;
19
- /** Optional naming strategy */
20
- namingStrategy?: NamingStrategy;
21
- // model registrations etc.
22
- }
13
+ /**
14
+ * Options for creating an ORM instance.
15
+ */
16
+ export interface OrmOptions {
17
+ /** The database dialect */
18
+ dialect: Dialect;
19
+ /** The database executor factory */
20
+ executorFactory: DbExecutorFactory;
21
+ /** Optional interceptors pipeline */
22
+ interceptors?: InterceptorPipeline;
23
+ /** Optional naming strategy */
24
+ namingStrategy?: NamingStrategy;
25
+ /** Optional cache configuration */
26
+ cache?: OrmCacheOptions;
27
+ }
28
+
29
+ /**
30
+ * Cache configuration options for ORM
31
+ */
32
+ export interface OrmCacheOptions {
33
+ /** Cache provider (e.g., MemoryCacheAdapter, KeyvCacheAdapter) */
34
+ provider: CacheProvider;
35
+ /** Optional cache strategy (defaults to DefaultCacheStrategy) */
36
+ strategy?: CacheStrategy;
37
+ /** Default TTL for cached queries (e.g., '1h', '30m') */
38
+ defaultTtl?: Duration;
39
+ }
23
40
 
24
41
  /**
25
42
  * Database executor factory interface.
@@ -43,58 +60,78 @@ export interface DbExecutorFactory {
43
60
  dispose(): Promise<void>;
44
61
  }
45
62
 
46
- /**
47
- * ORM (Object-Relational Mapping) main class.
48
- * @template E - The domain event type
49
- */
50
- export class Orm<E extends DomainEvent = OrmDomainEvent> {
51
- /** The database dialect */
52
- readonly dialect: Dialect;
53
- /** The interceptors pipeline */
54
- readonly interceptors: InterceptorPipeline;
55
- /** The naming strategy */
56
- readonly namingStrategy: NamingStrategy;
57
- private readonly executorFactory: DbExecutorFactory;
63
+ /**
64
+ * ORM (Object-Relational Mapping) main class.
65
+ * @template E - The domain event type
66
+ */
67
+ export class Orm<E extends DomainEvent = OrmDomainEvent> {
68
+ /** The database dialect */
69
+ readonly dialect: Dialect;
70
+ /** The interceptors pipeline */
71
+ readonly interceptors: InterceptorPipeline;
72
+ /** The naming strategy */
73
+ readonly namingStrategy: NamingStrategy;
74
+ /** The cache manager (if configured) */
75
+ readonly cacheManager?: QueryCacheManager;
76
+ private readonly executorFactory: DbExecutorFactory;
77
+
78
+ /**
79
+ * Creates a new ORM instance.
80
+ * @param opts - ORM options
81
+ */
82
+ constructor(opts: OrmOptions) {
83
+ this.dialect = opts.dialect;
84
+ this.interceptors = opts.interceptors ?? new InterceptorPipeline();
85
+ this.namingStrategy = opts.namingStrategy ?? new DefaultNamingStrategy();
86
+ this.executorFactory = opts.executorFactory;
87
+
88
+ // Initialize cache manager if cache options provided
89
+ if (opts.cache) {
90
+ this.cacheManager = new QueryCacheManagerImpl(
91
+ opts.cache.provider,
92
+ opts.cache.strategy ?? new DefaultCacheStrategy(),
93
+ opts.cache.defaultTtl ?? '1h'
94
+ );
95
+ }
96
+ }
58
97
 
59
- /**
60
- * Creates a new ORM instance.
61
- * @param opts - ORM options
62
- */
63
- constructor(opts: OrmOptions) {
64
- this.dialect = opts.dialect;
65
- this.interceptors = opts.interceptors ?? new InterceptorPipeline();
66
- this.namingStrategy = opts.namingStrategy ?? new DefaultNamingStrategy();
67
- this.executorFactory = opts.executorFactory;
68
- }
98
+ /**
99
+ * Creates a new ORM session.
100
+ * @param options - Optional session options (e.g., tenantId for multi-tenancy)
101
+ * @returns The ORM session
102
+ */
103
+ createSession(options?: { tenantId?: string | number }): OrmSession<E> {
104
+ // No implicit transaction binding; callers should use Orm.transaction() for transactional work.
105
+ const executor = this.executorFactory.createExecutor();
106
+ return new OrmSession<E>({
107
+ orm: this,
108
+ executor,
109
+ cacheManager: this.cacheManager,
110
+ tenantId: options?.tenantId
111
+ });
112
+ }
69
113
 
70
- /**
71
- * Creates a new ORM session.
72
- * @param options - Optional session options
73
- * @returns The ORM session
74
- */
75
- createSession(): OrmSession<E> {
76
- // No implicit transaction binding; callers should use Orm.transaction() for transactional work.
77
- const executor = this.executorFactory.createExecutor();
78
- return new OrmSession<E>({ orm: this, executor });
79
- }
80
-
81
- /**
82
- * Executes a function within a transaction.
83
- * @template T - The return type
84
- * @param fn - The function to execute
85
- * @returns The result of the function
86
- * @throws If the transaction fails
87
- */
88
- async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
89
- const executor = this.executorFactory.createTransactionalExecutor();
90
- const session = new OrmSession<E>({ orm: this, executor });
91
- try {
92
- // A real transaction scope: begin before running user code, commit/rollback after.
93
- return await session.transaction(() => fn(session));
94
- } finally {
95
- await session.dispose();
96
- }
97
- }
114
+ /**
115
+ * Executes a function within a transaction.
116
+ * @template T - The return type
117
+ * @param fn - The function to execute
118
+ * @returns The result of the function
119
+ * @throws If the transaction fails
120
+ */
121
+ async transaction<T>(fn: (session: OrmSession<E>) => Promise<T>): Promise<T> {
122
+ const executor = this.executorFactory.createTransactionalExecutor();
123
+ const session = new OrmSession<E>({
124
+ orm: this,
125
+ executor,
126
+ cacheManager: this.cacheManager
127
+ });
128
+ try {
129
+ // A real transaction scope: begin before running user code, commit/rollback after.
130
+ return await session.transaction(() => fn(session));
131
+ } finally {
132
+ await session.dispose();
133
+ }
134
+ }
98
135
 
99
136
  /**
100
137
  * Shuts down the ORM and releases underlying resources (pools, timers).
@@ -198,12 +198,13 @@ export class UnitOfWork {
198
198
 
199
199
  const payload = this.extractColumns(tracked.table, tracked.entity as Record<string, unknown>);
200
200
  let builder = new InsertQueryBuilder(tracked.table).values(payload as Record<string, ValueOperandInput>);
201
- if (this.dialect.supportsReturning()) {
201
+ if (this.dialect.supportsDmlReturningClause()) {
202
202
  builder = builder.returning(...this.getReturningColumns(tracked.table));
203
203
  }
204
204
  const compiled = builder.compile(this.dialect);
205
205
  const results = await this.executeCompiled(compiled);
206
206
  this.applyReturningResults(tracked, results);
207
+ this.applyInsertedIdIfAbsent(tracked, results);
207
208
 
208
209
  tracked.status = EntityStatus.Managed;
209
210
  tracked.original = this.createSnapshot(tracked.table, tracked.entity as Record<string, unknown>);
@@ -234,7 +235,7 @@ export class UnitOfWork {
234
235
  .set(changes)
235
236
  .where(eq(pkColumn, tracked.pk));
236
237
 
237
- if (this.dialect.supportsReturning()) {
238
+ if (this.dialect.supportsDmlReturningClause()) {
238
239
  builder = builder.returning(...this.getReturningColumns(tracked.table));
239
240
  }
240
241
 
@@ -345,9 +346,8 @@ export class UnitOfWork {
345
346
  * @param results - Query results
346
347
  */
347
348
  private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
348
- if (!this.dialect.supportsReturning()) return;
349
349
  const first = results[0];
350
- if (!first || first.values.length === 0) return;
350
+ if (!first || first.columns.length === 0 || first.values.length === 0) return;
351
351
 
352
352
  const row = first.values[0];
353
353
  for (let i = 0; i < first.columns.length; i++) {
@@ -357,6 +357,24 @@ export class UnitOfWork {
357
357
  }
358
358
  }
359
359
 
360
+ /**
361
+ * Applies the driver-provided insertId when no RETURNING clause was used.
362
+ * Only sets the PK if it is currently absent on the entity.
363
+ * @param tracked - The tracked entity
364
+ * @param results - Query results (may contain meta.insertId)
365
+ */
366
+ private applyInsertedIdIfAbsent(tracked: TrackedEntity, results: QueryResult[]): void {
367
+ const pkName = findPrimaryKey(tracked.table);
368
+ const current = (tracked.entity as Record<string, unknown>)[pkName];
369
+ if (current != null) return;
370
+
371
+ const first = results[0];
372
+ const insertId = first?.meta?.insertId;
373
+ if (insertId == null) return;
374
+
375
+ (tracked.entity as Record<string, unknown>)[pkName] = insertId;
376
+ }
377
+
360
378
  /**
361
379
  * Normalizes a column name by removing quotes and table prefixes.
362
380
  * @param column - The column name to normalize
@@ -0,0 +1,67 @@
1
+ import type { CacheOptions, CacheState, Duration } from '../../cache/cache-interfaces.js';
2
+
3
+ /**
4
+ * Facet para gerenciar estado de cache no SelectQueryBuilder
5
+ * Segue o padrão de facets existente no projeto
6
+ */
7
+ export interface CacheFacetContext {
8
+ state: CacheState;
9
+ }
10
+
11
+ export class CacheFacet {
12
+ /**
13
+ * Configura opções de cache no contexto
14
+ */
15
+ cache(
16
+ context: CacheFacetContext,
17
+ options: CacheOptions
18
+ ): CacheFacetContext {
19
+ return {
20
+ state: {
21
+ ...context.state,
22
+ options,
23
+ },
24
+ };
25
+ }
26
+
27
+ /**
28
+ * Obtém as opções de cache do contexto
29
+ */
30
+ getOptions(context: CacheFacetContext): CacheOptions | undefined {
31
+ return context.state.options;
32
+ }
33
+
34
+ /**
35
+ * Verifica se há configuração de cache
36
+ */
37
+ hasCache(context: CacheFacetContext): boolean {
38
+ return context.state.options !== undefined;
39
+ }
40
+
41
+ /**
42
+ * Cria opções de cache a partir de parâmetros variados
43
+ * API flexível para diferentes casos de uso
44
+ */
45
+ static createOptions(
46
+ key: string,
47
+ ttl: Duration,
48
+ tagsOrConfig?: string[] | { tags?: string[]; autoInvalidate?: boolean }
49
+ ): CacheOptions {
50
+ let tags: string[] | undefined;
51
+ let autoInvalidate: boolean | undefined;
52
+
53
+ if (Array.isArray(tagsOrConfig)) {
54
+ tags = tagsOrConfig;
55
+ } else if (tagsOrConfig) {
56
+ tags = tagsOrConfig.tags;
57
+ autoInvalidate = tagsOrConfig.autoInvalidate;
58
+ }
59
+
60
+ return {
61
+ key,
62
+ ttl,
63
+ tags,
64
+ autoInvalidate,
65
+ };
66
+ }
67
+ }
@@ -67,9 +67,11 @@ import { SelectProjectionFacet } from './select/projection-facet.js';
67
67
  import { SelectPredicateFacet } from './select/predicate-facet.js';
68
68
  import { SelectCTEFacet } from './select/cte-facet.js';
69
69
  import { SelectSetOpFacet } from './select/setop-facet.js';
70
- import { SelectRelationFacet } from './select/relation-facet.js';
71
-
72
- type ColumnSelectionValue =
70
+ import { SelectRelationFacet } from './select/relation-facet.js';
71
+ import { CacheFacet, CacheFacetContext } from './select/cache-facet.js';
72
+ import type { Duration } from '../cache/cache-interfaces.js';
73
+
74
+ type ColumnSelectionValue =
73
75
  | ColumnDef
74
76
  | FunctionNode
75
77
  | CaseExpressionNode
@@ -119,26 +121,29 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
119
121
  private readonly relationFacet: SelectRelationFacet;
120
122
  private readonly lazyRelations: Set<string>;
121
123
  private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
122
- private readonly entityConstructor?: EntityConstructor;
123
- private readonly includeTree: NormalizedRelationIncludeTree;
124
-
125
- /**
126
- * Creates a new SelectQueryBuilder instance
124
+ private readonly entityConstructor?: EntityConstructor;
125
+ private readonly includeTree: NormalizedRelationIncludeTree;
126
+ private readonly cacheFacet: CacheFacet;
127
+ private readonly cacheContext: CacheFacetContext;
128
+
129
+ /**
130
+ * Creates a new SelectQueryBuilder instance
127
131
  * @param table - Table definition to query
128
132
  * @param state - Optional initial query state
129
133
  * @param hydration - Optional hydration manager
130
134
  * @param dependencies - Optional query builder dependencies
131
135
  */
132
- constructor(
133
- table: TTable,
134
- state?: SelectQueryState,
135
- hydration?: HydrationManager,
136
- dependencies?: Partial<SelectQueryBuilderDependencies>,
137
- lazyRelations?: Set<string>,
138
- lazyRelationOptions?: Map<string, RelationIncludeOptions>,
139
- entityConstructor?: EntityConstructor,
140
- includeTree?: NormalizedRelationIncludeTree
141
- ) {
136
+ constructor(
137
+ table: TTable,
138
+ state?: SelectQueryState,
139
+ hydration?: HydrationManager,
140
+ dependencies?: Partial<SelectQueryBuilderDependencies>,
141
+ lazyRelations?: Set<string>,
142
+ lazyRelationOptions?: Map<string, RelationIncludeOptions>,
143
+ entityConstructor?: EntityConstructor,
144
+ includeTree?: NormalizedRelationIncludeTree,
145
+ cacheContext?: CacheFacetContext
146
+ ) {
142
147
  const deps = resolveSelectQueryBuilderDependencies(dependencies);
143
148
  this.env = { table, deps };
144
149
  const createAstService = (nextState: SelectQueryState) => deps.createQueryAstService(table, nextState);
@@ -150,9 +155,11 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
150
155
  };
151
156
  this.lazyRelations = new Set(lazyRelations ?? []);
152
157
  this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
153
- this.entityConstructor = entityConstructor;
154
- this.includeTree = includeTree ?? {};
155
- this.columnSelector = deps.createColumnSelector(this.env);
158
+ this.entityConstructor = entityConstructor;
159
+ this.includeTree = includeTree ?? {};
160
+ this.cacheFacet = new CacheFacet();
161
+ this.cacheContext = cacheContext ?? { state: {} };
162
+ this.columnSelector = deps.createColumnSelector(this.env);
156
163
  const relationManager = deps.createRelationManager(this.env);
157
164
  this.fromFacet = new SelectFromFacet(this.env, createAstService);
158
165
  this.joinFacet = new SelectJoinFacet(this.env, createAstService);
@@ -169,23 +176,25 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
169
176
  * @param lazyRelations - Updated lazy relations set
170
177
  * @returns New SelectQueryBuilder instance
171
178
  */
172
- private clone<TNext = T>(
173
- context: SelectQueryBuilderContext = this.context,
174
- lazyRelations = new Set(this.lazyRelations),
175
- lazyRelationOptions = new Map(this.lazyRelationOptions),
176
- includeTree = this.includeTree
177
- ): SelectQueryBuilder<TNext, TTable> {
178
- return new SelectQueryBuilder(
179
- this.env.table as TTable,
180
- context.state,
181
- context.hydration,
182
- this.env.deps,
183
- lazyRelations,
184
- lazyRelationOptions,
185
- this.entityConstructor,
186
- includeTree
187
- ) as SelectQueryBuilder<TNext, TTable>;
188
- }
179
+ private clone<TNext = T>(
180
+ context: SelectQueryBuilderContext = this.context,
181
+ lazyRelations = new Set(this.lazyRelations),
182
+ lazyRelationOptions = new Map(this.lazyRelationOptions),
183
+ includeTree = this.includeTree,
184
+ cacheContext = this.cacheContext
185
+ ): SelectQueryBuilder<TNext, TTable> {
186
+ return new SelectQueryBuilder(
187
+ this.env.table as TTable,
188
+ context.state,
189
+ context.hydration,
190
+ this.env.deps,
191
+ lazyRelations,
192
+ lazyRelationOptions,
193
+ this.entityConstructor,
194
+ includeTree,
195
+ cacheContext
196
+ ) as SelectQueryBuilder<TNext, TTable>;
197
+ }
189
198
 
190
199
  /**
191
200
  * Applies an alias to the root FROM table.
@@ -741,25 +750,84 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
741
750
  return this;
742
751
  }
743
752
 
744
- /**
745
- * Executes the query and returns hydrated results.
746
- * If the builder was created with an entity constructor (e.g. via selectFromEntity),
747
- * this will automatically return fully materialized entity instances.
748
- *
749
- * @param ctx - ORM session context
750
- * @returns Promise of entity instances (or objects if generic T is not an entity)
751
- * @example
752
- * const users = await selectFromEntity(User).execute(session);
753
- * // users is User[]
754
- * users[0] instanceof User; // true
755
- */
756
- async execute(ctx: OrmSession): Promise<T[]> {
757
- if (this.entityConstructor) {
758
- return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
759
- }
760
- const builder = this.ensureDefaultSelection();
761
- return executeHydrated(ctx, builder) as unknown as T[];
762
- }
753
+ /**
754
+ * Configures caching for this query.
755
+ * @param key - Unique cache key
756
+ * @param ttl - Time-to-live (e.g., '1h', '30m', '1d') or milliseconds
757
+ * @param tagsOrConfig - Optional tags for invalidation or configuration object
758
+ * @returns New query builder instance with cache configuration
759
+ * @example
760
+ * // Simple cache with TTL
761
+ * await selectFrom(User).cache('active_users', '1h').execute(session);
762
+ *
763
+ * // Cache with tags for invalidation
764
+ * await selectFrom(User)
765
+ * .cache('users_list', '30m', ['users', 'dashboard'])
766
+ * .execute(session);
767
+ *
768
+ * // Cache with auto-invalidation
769
+ * await selectFrom(User)
770
+ * .cache('users_list', '1h', { autoInvalidate: true })
771
+ * .execute(session);
772
+ */
773
+ cache(
774
+ key: string,
775
+ ttl: Duration,
776
+ tagsOrConfig?: string[] | { tags?: string[]; autoInvalidate?: boolean }
777
+ ): SelectQueryBuilder<T, TTable> {
778
+ const options = CacheFacet.createOptions(key, ttl, tagsOrConfig);
779
+ const nextCacheContext = this.cacheFacet.cache(this.cacheContext, options);
780
+
781
+ const builder = this.clone(
782
+ this.context,
783
+ new Set(this.lazyRelations),
784
+ new Map(this.lazyRelationOptions),
785
+ this.includeTree,
786
+ nextCacheContext
787
+ );
788
+
789
+ return builder;
790
+ }
791
+
792
+ /**
793
+ * Executes the query and returns hydrated results.
794
+ * If the builder was created with an entity constructor (e.g. via selectFromEntity),
795
+ * this will automatically return fully materialized entity instances.
796
+ * If caching is configured, results will be cached/retrieved from cache.
797
+ *
798
+ * @param ctx - ORM session context
799
+ * @returns Promise of entity instances (or objects if generic T is not an entity)
800
+ * @example
801
+ * const users = await selectFromEntity(User).execute(session);
802
+ * // users is User[]
803
+ * users[0] instanceof User; // true
804
+ */
805
+ async execute(ctx: OrmSession): Promise<T[]> {
806
+ const cacheOptions = this.cacheFacet.getOptions(this.cacheContext);
807
+
808
+ // Se não tem cache configurado ou não há cache manager, executa normalmente
809
+ if (!cacheOptions || !ctx.cacheManager) {
810
+ return this.executeWithoutCache(ctx);
811
+ }
812
+
813
+ // Executa com cache
814
+ return ctx.cacheManager.getOrExecute(
815
+ cacheOptions,
816
+ () => this.executeWithoutCache(ctx),
817
+ ctx.tenantId
818
+ );
819
+ }
820
+
821
+ /**
822
+ * Executa a query sem cache (método interno)
823
+ */
824
+ private async executeWithoutCache(ctx: OrmSession): Promise<T[]> {
825
+ if (this.entityConstructor) {
826
+ return this.executeAs(this.entityConstructor, ctx) as unknown as T[];
827
+ }
828
+ const builder = this.ensureDefaultSelection();
829
+ return executeHydrated(ctx, builder) as unknown as T[];
830
+ }
763
831
 
764
832
  /**
765
833
  * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.