metal-orm 1.0.60 → 1.0.62

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
@@ -149,7 +149,7 @@ If you like explicit model classes, you can add a thin decorator layer on top of
149
149
  - `@BelongsTo({ target, foreignKey, ... })`
150
150
  - `@BelongsToMany({ target, pivotTable, ... })`
151
151
  - `bootstrapEntities()` scans metadata, builds `TableDef`s, wires relations with the same `hasOne` / `hasMany` / `belongsTo` / `belongsToMany` helpers you would use manually, and returns the resulting tables. (If you forget to call it, `getTableDefFromEntity` / `selectFromEntity` will bootstrap lazily on first use, but bootstrapping once at startup lets you reuse the same table defs and generate schema SQL.)
152
- - `selectFromEntity(MyEntity)` lets you start a `SelectQueryBuilder` directly from the class.
152
+ - `selectFromEntity(MyEntity)` lets you start a `SelectQueryBuilder` directly from the class. By default, `execute(session)` returns actual entity instances with all columns selected.
153
153
  - **Generate entities from an existing DB**: `npx metal-orm-gen -- --dialect=postgres --url=$DATABASE_URL --schema=public --out=src/entities.ts` introspects your schema and spits out `@Entity` / `@Column` classes you can immediately `bootstrapEntities()` with.
154
154
 
155
155
  You don’t have to use decorators, but when you do, you’re still on the same AST + dialect + runtime foundation.
@@ -465,14 +465,14 @@ What the runtime gives you:
465
465
  <a id="level-3"></a>
466
466
  ### Level 3: Decorator entities ✨
467
467
 
468
- Finally, you can describe your models with decorators and still use the same runtime and query builder.
469
-
470
- The decorator layer is built on the TC39 Stage 3 standard (TypeScript 5.6+), so you simply decorate class fields (or accessors if you need custom logic) and the standard `ClassFieldDecoratorContext` keeps a metadata bag on `context.metadata`/`Symbol.metadata`. `@Entity` reads that bag when it runs and builds your `TableDef`s—no `experimentalDecorators`, parameter decorators, or extra polyfills required.
471
-
472
- ```ts
473
- import mysql from 'mysql2/promise';
474
- import {
475
- Orm,
468
+ Finally, you can describe your models with decorators and still use the same runtime and query builder.
469
+
470
+ The decorator layer is built on the TC39 Stage 3 standard (TypeScript 5.6+), so you simply decorate class fields (or accessors if you need custom logic) and the standard `ClassFieldDecoratorContext` keeps a metadata bag on `context.metadata`/`Symbol.metadata`. `@Entity` reads that bag when it runs and builds your `TableDef`s—no `experimentalDecorators`, parameter decorators, or extra polyfills required.
471
+
472
+ ```ts
473
+ import mysql from 'mysql2/promise';
474
+ import {
475
+ Orm,
476
476
  OrmSession,
477
477
  MySqlDialect,
478
478
  col,
@@ -547,13 +547,17 @@ const [user] = await selectFromEntity(User)
547
547
  .select('id', 'name')
548
548
  .includeLazy('posts')
549
549
  .where(eq(U.id, 1))
550
- .execute(session);
550
+ .execute(session); // user is an actual instance of the User class!
551
+
552
+ // Use executePlain() if you want raw POJOs instead of class instances
553
+ const [rawUser] = await selectFromEntity(User).executePlain(session);
551
554
 
552
555
  user.posts.add({ title: 'From decorators' });
553
556
  await session.commit();
554
557
  ```
555
558
 
556
- Tip: to keep selections terse, use `select`, `include` (with `columns`), or the `sel`/`esel` helpers instead of spelling `table.columns.*` over and over.
559
+ Tip: to keep selections terse, use `select`, `include` (with `columns`), or the `sel`/`esel` helpers instead of spelling `table.columns.*` over and over. By default, `selectFromEntity` selects all columns if you don't specify any.
560
+
557
561
 
558
562
  This level is nice when:
559
563
 
package/dist/index.cjs CHANGED
@@ -47,7 +47,9 @@ __export(index_exports, {
47
47
  BelongsTo: () => BelongsTo,
48
48
  BelongsToMany: () => BelongsToMany,
49
49
  Column: () => Column,
50
+ ConstructorMaterializationStrategy: () => ConstructorMaterializationStrategy,
50
51
  DefaultBelongsToReference: () => DefaultBelongsToReference,
52
+ DefaultEntityMaterializer: () => DefaultEntityMaterializer,
51
53
  DefaultHasManyCollection: () => DefaultHasManyCollection,
52
54
  DefaultManyToManyCollection: () => DefaultManyToManyCollection,
53
55
  DeleteQueryBuilder: () => DeleteQueryBuilder,
@@ -63,6 +65,7 @@ __export(index_exports, {
63
65
  Pool: () => Pool,
64
66
  PostgresDialect: () => PostgresDialect,
65
67
  PrimaryKey: () => PrimaryKey,
68
+ PrototypeMaterializationStrategy: () => PrototypeMaterializationStrategy,
66
69
  RelationKinds: () => RelationKinds,
67
70
  STANDARD_COLUMN_TYPES: () => STANDARD_COLUMN_TYPES,
68
71
  SelectQueryBuilder: () => SelectQueryBuilder,
@@ -214,6 +217,7 @@ __export(index_exports, {
214
217
  lt: () => lt,
215
218
  lte: () => lte,
216
219
  ltrim: () => ltrim,
220
+ materializeAs: () => materializeAs,
217
221
  max: () => max,
218
222
  md5: () => md5,
219
223
  min: () => min,
@@ -6238,6 +6242,132 @@ var loadLazyRelationsForTable = async (ctx, table, lazyRelations, lazyRelationOp
6238
6242
  }
6239
6243
  };
6240
6244
 
6245
+ // src/orm/entity-metadata.ts
6246
+ var metadataMap = /* @__PURE__ */ new Map();
6247
+ var ensureEntityMetadata = (target) => {
6248
+ let meta = metadataMap.get(target);
6249
+ if (!meta) {
6250
+ meta = {
6251
+ target,
6252
+ tableName: target.name || "unknown",
6253
+ columns: {},
6254
+ relations: {}
6255
+ };
6256
+ metadataMap.set(target, meta);
6257
+ }
6258
+ return meta;
6259
+ };
6260
+ var getEntityMetadata = (target) => {
6261
+ return metadataMap.get(target);
6262
+ };
6263
+ var getAllEntityMetadata = () => {
6264
+ return Array.from(metadataMap.values());
6265
+ };
6266
+ var addColumnMetadata = (target, propertyKey, column) => {
6267
+ const meta = ensureEntityMetadata(target);
6268
+ meta.columns[propertyKey] = { ...column };
6269
+ };
6270
+ var addRelationMetadata = (target, propertyKey, relation) => {
6271
+ const meta = ensureEntityMetadata(target);
6272
+ meta.relations[propertyKey] = relation;
6273
+ };
6274
+ var setEntityTableName = (target, tableName, hooks) => {
6275
+ const meta = ensureEntityMetadata(target);
6276
+ if (tableName && tableName.length > 0) {
6277
+ meta.tableName = tableName;
6278
+ }
6279
+ if (hooks) {
6280
+ meta.hooks = hooks;
6281
+ }
6282
+ };
6283
+ var buildTableDef = (meta) => {
6284
+ if (meta.table) {
6285
+ return meta.table;
6286
+ }
6287
+ const columns = {};
6288
+ for (const [key, def] of Object.entries(meta.columns)) {
6289
+ columns[key] = {
6290
+ ...def,
6291
+ name: key,
6292
+ table: meta.tableName
6293
+ };
6294
+ }
6295
+ const table = defineTable(meta.tableName, columns, {}, meta.hooks);
6296
+ meta.table = table;
6297
+ return table;
6298
+ };
6299
+
6300
+ // src/orm/entity-registry.ts
6301
+ var tableToConstructor = /* @__PURE__ */ new Map();
6302
+ var rebuildRegistry = () => {
6303
+ tableToConstructor.clear();
6304
+ for (const meta of getAllEntityMetadata()) {
6305
+ if (meta.table) {
6306
+ tableToConstructor.set(meta.table, meta.target);
6307
+ }
6308
+ }
6309
+ };
6310
+
6311
+ // src/orm/entity-materializer.ts
6312
+ var PrototypeMaterializationStrategy = class {
6313
+ materialize(ctor, data) {
6314
+ const instance = Object.create(ctor.prototype);
6315
+ Object.assign(instance, data);
6316
+ return instance;
6317
+ }
6318
+ };
6319
+ var ConstructorMaterializationStrategy = class {
6320
+ materialize(ctor, data) {
6321
+ const instance = Reflect.construct(ctor, []);
6322
+ Object.assign(instance, data);
6323
+ return instance;
6324
+ }
6325
+ };
6326
+ var DefaultEntityMaterializer = class {
6327
+ constructor(strategy = new ConstructorMaterializationStrategy()) {
6328
+ this.strategy = strategy;
6329
+ }
6330
+ materialize(ctor, row) {
6331
+ const instance = this.strategy.materialize(ctor, row);
6332
+ this.materializeRelations(instance, ctor);
6333
+ return instance;
6334
+ }
6335
+ materializeMany(ctor, rows) {
6336
+ return rows.map((row) => this.materialize(ctor, row));
6337
+ }
6338
+ /**
6339
+ * Recursively materializes nested relation data.
6340
+ */
6341
+ materializeRelations(instance, _ctor) {
6342
+ rebuildRegistry();
6343
+ for (const [key, value] of Object.entries(instance)) {
6344
+ if (value === null || value === void 0) continue;
6345
+ if (typeof value === "object" && !Array.isArray(value)) {
6346
+ const nested = value;
6347
+ if (this.isEntityLike(nested)) {
6348
+ }
6349
+ }
6350
+ if (Array.isArray(value) && value.length > 0) {
6351
+ const first = value[0];
6352
+ if (typeof first === "object" && first !== null && this.isEntityLike(first)) {
6353
+ }
6354
+ }
6355
+ }
6356
+ }
6357
+ /**
6358
+ * Simple heuristic to check if an object looks like an entity.
6359
+ */
6360
+ isEntityLike(obj) {
6361
+ return "id" in obj || Object.keys(obj).some(
6362
+ (k) => k.endsWith("Id") || k === "createdAt" || k === "updatedAt"
6363
+ );
6364
+ }
6365
+ };
6366
+ var materializeAs = (ctor, results) => {
6367
+ const materializer = new DefaultEntityMaterializer();
6368
+ return materializer.materializeMany(ctor, results);
6369
+ };
6370
+
6241
6371
  // src/query-builder/query-resolution.ts
6242
6372
  function resolveSelectQuery(query) {
6243
6373
  const candidate = query;
@@ -6648,6 +6778,7 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
6648
6778
  relationFacet;
6649
6779
  lazyRelations;
6650
6780
  lazyRelationOptions;
6781
+ entityConstructor;
6651
6782
  /**
6652
6783
  * Creates a new SelectQueryBuilder instance
6653
6784
  * @param table - Table definition to query
@@ -6655,7 +6786,7 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
6655
6786
  * @param hydration - Optional hydration manager
6656
6787
  * @param dependencies - Optional query builder dependencies
6657
6788
  */
6658
- constructor(table, state, hydration, dependencies, lazyRelations, lazyRelationOptions) {
6789
+ constructor(table, state, hydration, dependencies, lazyRelations, lazyRelationOptions, entityConstructor) {
6659
6790
  const deps = resolveSelectQueryBuilderDependencies(dependencies);
6660
6791
  this.env = { table, deps };
6661
6792
  const createAstService = (nextState) => deps.createQueryAstService(table, nextState);
@@ -6667,6 +6798,7 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
6667
6798
  };
6668
6799
  this.lazyRelations = new Set(lazyRelations ?? []);
6669
6800
  this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
6801
+ this.entityConstructor = entityConstructor;
6670
6802
  this.columnSelector = deps.createColumnSelector(this.env);
6671
6803
  const relationManager = deps.createRelationManager(this.env);
6672
6804
  this.fromFacet = new SelectFromFacet(this.env, createAstService);
@@ -6690,7 +6822,8 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
6690
6822
  context.hydration,
6691
6823
  this.env.deps,
6692
6824
  lazyRelations,
6693
- lazyRelationOptions
6825
+ lazyRelationOptions,
6826
+ this.entityConstructor
6694
6827
  );
6695
6828
  }
6696
6829
  /**
@@ -7081,16 +7214,67 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
7081
7214
  return this.env.table;
7082
7215
  }
7083
7216
  /**
7084
- * Executes the query and returns hydrated results
7217
+ * Ensures that if no columns are selected, all columns from the table are selected by default.
7218
+ */
7219
+ ensureDefaultSelection() {
7220
+ const columns = this.context.state.ast.columns;
7221
+ if (!columns || columns.length === 0) {
7222
+ return this.select(...Object.keys(this.env.table.columns));
7223
+ }
7224
+ return this;
7225
+ }
7226
+ /**
7227
+ * Executes the query and returns hydrated results.
7228
+ * If the builder was created with an entity constructor (e.g. via selectFromEntity),
7229
+ * this will automatically return fully materialized entity instances.
7230
+ *
7085
7231
  * @param ctx - ORM session context
7086
- * @returns Promise of entity instances
7232
+ * @returns Promise of entity instances (or objects if generic T is not an entity)
7087
7233
  * @example
7088
- * const users = await qb.select('id', 'name')
7089
- * .where(eq(userTable.columns.active, true))
7090
- * .execute(session);
7234
+ * const users = await selectFromEntity(User).execute(session);
7235
+ * // users is User[]
7236
+ * users[0] instanceof User; // true
7091
7237
  */
7092
7238
  async execute(ctx) {
7093
- return executeHydrated(ctx, this);
7239
+ if (this.entityConstructor) {
7240
+ return this.executeAs(this.entityConstructor, ctx);
7241
+ }
7242
+ const builder = this.ensureDefaultSelection();
7243
+ return executeHydrated(ctx, builder);
7244
+ }
7245
+ /**
7246
+ * Executes the query and returns plain row objects (POJOs), ignoring any entity materialization.
7247
+ * Use this if you want raw data even when using selectFromEntity.
7248
+ *
7249
+ * @param ctx - ORM session context
7250
+ * @returns Promise of plain entity instances
7251
+ * @example
7252
+ * const rows = await selectFromEntity(User).executePlain(session);
7253
+ * // rows is EntityInstance<UserTable>[] (plain objects)
7254
+ * rows[0] instanceof User; // false
7255
+ */
7256
+ async executePlain(ctx) {
7257
+ const builder = this.ensureDefaultSelection();
7258
+ return executeHydrated(ctx, builder);
7259
+ }
7260
+ /**
7261
+ * Executes the query and returns results as real class instances.
7262
+ * Unlike execute(), this returns actual instances of the decorated entity class
7263
+ * with working methods and proper instanceof checks.
7264
+ * @param entityClass - The entity class constructor
7265
+ * @param ctx - ORM session context
7266
+ * @returns Promise of entity class instances
7267
+ * @example
7268
+ * const users = await selectFromEntity(User)
7269
+ * .include('posts')
7270
+ * .executeAs(User, session);
7271
+ * users[0] instanceof User; // true!
7272
+ * users[0].getFullName(); // works!
7273
+ */
7274
+ async executeAs(entityClass, ctx) {
7275
+ const builder = this.ensureDefaultSelection();
7276
+ const results = await executeHydrated(ctx, builder);
7277
+ return materializeAs(entityClass, results);
7094
7278
  }
7095
7279
  /**
7096
7280
  * Executes a count query for the current builder without LIMIT/OFFSET clauses.
@@ -7108,7 +7292,8 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
7108
7292
  * const { items, totalItems } = await qb.executePaged(session, { page: 1, pageSize: 20 });
7109
7293
  */
7110
7294
  async executePaged(session, options) {
7111
- return executePagedQuery(this, session, options, (sess) => this.count(sess));
7295
+ const builder = this.ensureDefaultSelection();
7296
+ return executePagedQuery(builder, session, options, (sess) => this.count(sess));
7112
7297
  }
7113
7298
  /**
7114
7299
  * Executes the query with provided execution and hydration contexts
@@ -7121,7 +7306,12 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
7121
7306
  * const users = await qb.executeWithContexts(execCtx, hydCtx);
7122
7307
  */
7123
7308
  async executeWithContexts(execCtx, hydCtx) {
7124
- return executeHydratedWithContexts(execCtx, hydCtx, this);
7309
+ const builder = this.ensureDefaultSelection();
7310
+ const results = await executeHydratedWithContexts(execCtx, hydCtx, builder);
7311
+ if (this.entityConstructor) {
7312
+ return materializeAs(this.entityConstructor, results);
7313
+ }
7314
+ return results;
7125
7315
  }
7126
7316
  /**
7127
7317
  * Adds a WHERE condition to the query
@@ -7414,61 +7604,6 @@ var isTableDef = (value) => {
7414
7604
  return true;
7415
7605
  };
7416
7606
 
7417
- // src/orm/entity-metadata.ts
7418
- var metadataMap = /* @__PURE__ */ new Map();
7419
- var ensureEntityMetadata = (target) => {
7420
- let meta = metadataMap.get(target);
7421
- if (!meta) {
7422
- meta = {
7423
- target,
7424
- tableName: target.name || "unknown",
7425
- columns: {},
7426
- relations: {}
7427
- };
7428
- metadataMap.set(target, meta);
7429
- }
7430
- return meta;
7431
- };
7432
- var getEntityMetadata = (target) => {
7433
- return metadataMap.get(target);
7434
- };
7435
- var getAllEntityMetadata = () => {
7436
- return Array.from(metadataMap.values());
7437
- };
7438
- var addColumnMetadata = (target, propertyKey, column) => {
7439
- const meta = ensureEntityMetadata(target);
7440
- meta.columns[propertyKey] = { ...column };
7441
- };
7442
- var addRelationMetadata = (target, propertyKey, relation) => {
7443
- const meta = ensureEntityMetadata(target);
7444
- meta.relations[propertyKey] = relation;
7445
- };
7446
- var setEntityTableName = (target, tableName, hooks) => {
7447
- const meta = ensureEntityMetadata(target);
7448
- if (tableName && tableName.length > 0) {
7449
- meta.tableName = tableName;
7450
- }
7451
- if (hooks) {
7452
- meta.hooks = hooks;
7453
- }
7454
- };
7455
- var buildTableDef = (meta) => {
7456
- if (meta.table) {
7457
- return meta.table;
7458
- }
7459
- const columns = {};
7460
- for (const [key, def] of Object.entries(meta.columns)) {
7461
- columns[key] = {
7462
- ...def,
7463
- name: key,
7464
- table: meta.tableName
7465
- };
7466
- }
7467
- const table = defineTable(meta.tableName, columns, {}, meta.hooks);
7468
- meta.table = table;
7469
- return table;
7470
- };
7471
-
7472
7607
  // src/decorators/bootstrap.ts
7473
7608
  var unwrapTarget = (target) => {
7474
7609
  if (typeof target === "function" && target.prototype === void 0) {
@@ -7588,7 +7723,15 @@ var selectFromEntity = (ctor) => {
7588
7723
  if (!table) {
7589
7724
  throw new Error(`Entity '${ctor.name}' is not registered with decorators or has not been bootstrapped`);
7590
7725
  }
7591
- return new SelectQueryBuilder(table);
7726
+ return new SelectQueryBuilder(
7727
+ table,
7728
+ void 0,
7729
+ void 0,
7730
+ void 0,
7731
+ void 0,
7732
+ void 0,
7733
+ ctor
7734
+ );
7592
7735
  };
7593
7736
  var entityRef = (ctor) => {
7594
7737
  const table = getTableDefFromEntity(ctor);
@@ -12834,7 +12977,9 @@ function createPooledExecutorFactory(opts) {
12834
12977
  BelongsTo,
12835
12978
  BelongsToMany,
12836
12979
  Column,
12980
+ ConstructorMaterializationStrategy,
12837
12981
  DefaultBelongsToReference,
12982
+ DefaultEntityMaterializer,
12838
12983
  DefaultHasManyCollection,
12839
12984
  DefaultManyToManyCollection,
12840
12985
  DeleteQueryBuilder,
@@ -12850,6 +12995,7 @@ function createPooledExecutorFactory(opts) {
12850
12995
  Pool,
12851
12996
  PostgresDialect,
12852
12997
  PrimaryKey,
12998
+ PrototypeMaterializationStrategy,
12853
12999
  RelationKinds,
12854
13000
  STANDARD_COLUMN_TYPES,
12855
13001
  SelectQueryBuilder,
@@ -13001,6 +13147,7 @@ function createPooledExecutorFactory(opts) {
13001
13147
  lt,
13002
13148
  lte,
13003
13149
  ltrim,
13150
+ materializeAs,
13004
13151
  max,
13005
13152
  md5,
13006
13153
  min,