metal-orm 1.0.16 → 1.0.17

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.
Files changed (45) hide show
  1. package/README.md +33 -37
  2. package/dist/decorators/index.cjs +152 -23
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -1
  5. package/dist/decorators/index.d.ts +1 -1
  6. package/dist/decorators/index.js +152 -23
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +322 -115
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +53 -4
  11. package/dist/index.d.ts +53 -4
  12. package/dist/index.js +316 -115
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-BKZrMRCQ.d.cts → select-BPCn6MOH.d.cts} +183 -64
  15. package/dist/{select-BKZrMRCQ.d.ts → select-BPCn6MOH.d.ts} +183 -64
  16. package/package.json +2 -1
  17. package/src/core/ast/aggregate-functions.ts +50 -4
  18. package/src/core/ast/expression-builders.ts +22 -15
  19. package/src/core/ast/expression-nodes.ts +6 -0
  20. package/src/core/ddl/introspect/functions/postgres.ts +2 -6
  21. package/src/core/dialect/abstract.ts +12 -8
  22. package/src/core/dialect/mssql/functions.ts +24 -15
  23. package/src/core/dialect/postgres/functions.ts +33 -24
  24. package/src/core/dialect/sqlite/functions.ts +19 -12
  25. package/src/core/functions/datetime.ts +2 -1
  26. package/src/core/functions/numeric.ts +2 -1
  27. package/src/core/functions/standard-strategy.ts +52 -12
  28. package/src/core/functions/text.ts +2 -1
  29. package/src/core/functions/types.ts +8 -8
  30. package/src/index.ts +5 -4
  31. package/src/orm/domain-event-bus.ts +43 -25
  32. package/src/orm/entity-meta.ts +40 -0
  33. package/src/orm/execution-context.ts +6 -0
  34. package/src/orm/hydration-context.ts +6 -4
  35. package/src/orm/orm-session.ts +35 -24
  36. package/src/orm/orm.ts +10 -10
  37. package/src/orm/query-logger.ts +15 -0
  38. package/src/orm/runtime-types.ts +60 -2
  39. package/src/orm/transaction-runner.ts +7 -0
  40. package/src/orm/unit-of-work.ts +1 -0
  41. package/src/query-builder/insert-query-state.ts +13 -3
  42. package/src/query-builder/select-helpers.ts +50 -0
  43. package/src/query-builder/select.ts +122 -30
  44. package/src/query-builder/update-query-state.ts +31 -9
  45. package/src/schema/types.ts +16 -6
package/README.md CHANGED
@@ -325,16 +325,12 @@ const orm = new Orm({
325
325
  const session = new OrmSession({ orm, executor });
326
326
 
327
327
  // 2) Load entities with lazy relations
328
- const [user] = await new SelectQueryBuilder(users)
329
- .select({
330
- id: users.columns.id,
331
- name: users.columns.name,
332
- email: users.columns.email,
333
- })
334
- .includeLazy('posts') // HasMany as a lazy collection
335
- .includeLazy('roles') // BelongsToMany as a lazy collection
336
- .where(eq(users.columns.id, 1))
337
- .execute(session);
328
+ const [user] = await new SelectQueryBuilder(users)
329
+ .selectColumns('id', 'name', 'email')
330
+ .includeLazy('posts') // HasMany as a lazy collection
331
+ .includeLazy('roles') // BelongsToMany as a lazy collection
332
+ .where(eq(users.columns.id, 1))
333
+ .execute(session);
338
334
 
339
335
  // user is an Entity<typeof users>
340
336
  // scalar props are normal:
@@ -355,10 +351,11 @@ await session.commit();
355
351
  What the runtime gives you:
356
352
 
357
353
  - [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) (per context).
358
- - [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) style change tracking on scalar properties.
359
- - Relation tracking (add/remove/sync on collections).
360
- - Cascades on relations: `'all' | 'persist' | 'remove' | 'link'`.
361
- - Single flush: `session.commit()` figures out inserts, updates, deletes, and pivot changes.
354
+ - [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) style change tracking on scalar properties.
355
+ - Relation tracking (add/remove/sync on collections).
356
+ - Cascades on relations: `'all' | 'persist' | 'remove' | 'link'`.
357
+ - Single flush: `session.commit()` figures out inserts, updates, deletes, and pivot changes.
358
+ - Column pickers to stay DRY: `selectColumns` on the root table, `selectRelationColumns` / `includePick` on relations, and `selectColumnsDeep` or the `sel`/`esel` helpers to build typed selection maps without repeating `table.columns.*`.
362
359
 
363
360
  <a id="level-3"></a>
364
361
  ### Level 3: Decorator entities ✨
@@ -370,11 +367,11 @@ Finally, you can describe your models with decorators and still use the same run
370
367
  ```ts
371
368
  import mysql from 'mysql2/promise';
372
369
  import { Orm, OrmSession, MySqlDialect, col, createMysqlExecutor } from 'metal-orm';
373
- import {
374
- Entity,
375
- Column,
376
- PrimaryKey,
377
- HasMany,
370
+ import {
371
+ Entity,
372
+ Column,
373
+ PrimaryKey,
374
+ HasMany,
378
375
  BelongsTo,
379
376
  bootstrapEntities,
380
377
  selectFromEntity,
@@ -432,24 +429,23 @@ const orm = new Orm({
432
429
  });
433
430
  const session = new OrmSession({ orm, executor });
434
431
 
435
- // 3) Query starting from the entity class
436
- const [user] = await selectFromEntity(User)
437
- .select({
438
- id: User.prototype.id, // or map to columns by name/alias as you prefer
439
- name: User.prototype.name,
440
- })
441
- .includeLazy('posts')
442
- .where(/* same eq()/and() API as before */)
443
- .execute(session);
444
-
445
- user.posts.add({ title: 'From decorators' });
446
- await session.commit();
447
- ```
448
-
449
- This level is nice when:
450
-
451
- - You want classes as your domain model, but don’t want a separate schema DSL.
452
- - You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
432
+ // 3) Query starting from the entity class
433
+ const [user] = await selectFromEntity(User)
434
+ .selectColumns('id', 'name')
435
+ .includeLazy('posts')
436
+ .where(/* same eq()/and() API as before */)
437
+ .execute(session);
438
+
439
+ user.posts.add({ title: 'From decorators' });
440
+ await session.commit();
441
+ ```
442
+
443
+ Tip: to keep selections terse, use `selectColumns`/`selectRelationColumns` or the `sel`/`esel` helpers instead of spelling `table.columns.*` over and over.
444
+
445
+ This level is nice when:
446
+
447
+ - You want classes as your domain model, but don't want a separate schema DSL.
448
+ - You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
453
449
 
454
450
  ---
455
451
 
@@ -332,6 +332,15 @@ var isWindowFunctionNode = (node) => node?.type === "WindowFunction";
332
332
  var isExpressionSelectionNode = (node) => isFunctionNode(node) || isCaseExpressionNode(node) || isWindowFunctionNode(node);
333
333
 
334
334
  // src/core/ast/expression-builders.ts
335
+ var valueToOperand = (value) => {
336
+ if (isOperandNode(value)) {
337
+ return value;
338
+ }
339
+ return {
340
+ type: "Literal",
341
+ value
342
+ };
343
+ };
335
344
  var toNode = (col) => {
336
345
  if (isOperandNode(col)) return col;
337
346
  const def = col;
@@ -341,10 +350,10 @@ var toLiteralNode = (value) => ({
341
350
  type: "Literal",
342
351
  value
343
352
  });
353
+ var isLiteralValue = (value) => value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
344
354
  var toOperand = (val) => {
345
- if (val === null) return { type: "Literal", value: null };
346
- if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
347
- return { type: "Literal", value: val };
355
+ if (isLiteralValue(val)) {
356
+ return valueToOperand(val);
348
357
  }
349
358
  return toNode(val);
350
359
  };
@@ -385,6 +394,24 @@ var notExists = (subquery) => ({
385
394
  subquery
386
395
  });
387
396
 
397
+ // src/core/sql/sql.ts
398
+ var JOIN_KINDS = {
399
+ /** INNER JOIN type */
400
+ INNER: "INNER",
401
+ /** LEFT JOIN type */
402
+ LEFT: "LEFT",
403
+ /** RIGHT JOIN type */
404
+ RIGHT: "RIGHT",
405
+ /** CROSS JOIN type */
406
+ CROSS: "CROSS"
407
+ };
408
+ var ORDER_DIRECTIONS = {
409
+ /** Ascending order */
410
+ ASC: "ASC",
411
+ /** Descending order */
412
+ DESC: "DESC"
413
+ };
414
+
388
415
  // src/core/ast/aggregate-functions.ts
389
416
  var buildAggregate = (name) => (col) => ({
390
417
  type: "Function",
@@ -394,14 +421,21 @@ var buildAggregate = (name) => (col) => ({
394
421
  var count = buildAggregate("COUNT");
395
422
  var sum = buildAggregate("SUM");
396
423
  var avg = buildAggregate("AVG");
424
+ var min = buildAggregate("MIN");
425
+ var max = buildAggregate("MAX");
397
426
 
398
427
  // src/core/functions/standard-strategy.ts
399
- var StandardFunctionStrategy = class {
428
+ var StandardFunctionStrategy = class _StandardFunctionStrategy {
400
429
  constructor() {
401
430
  this.renderers = /* @__PURE__ */ new Map();
402
431
  this.registerStandard();
403
432
  }
404
433
  registerStandard() {
434
+ this.add("COUNT", ({ compiledArgs }) => `COUNT(${compiledArgs.join(", ")})`);
435
+ this.add("SUM", ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
436
+ this.add("AVG", ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
437
+ this.add("MIN", ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
438
+ this.add("MAX", ({ compiledArgs }) => `MAX(${compiledArgs[0]})`);
405
439
  this.add("ABS", ({ compiledArgs }) => `ABS(${compiledArgs[0]})`);
406
440
  this.add("UPPER", ({ compiledArgs }) => `UPPER(${compiledArgs[0]})`);
407
441
  this.add("LOWER", ({ compiledArgs }) => `LOWER(${compiledArgs[0]})`);
@@ -428,6 +462,7 @@ var StandardFunctionStrategy = class {
428
462
  this.add("DAY_OF_WEEK", ({ compiledArgs }) => `DAYOFWEEK(${compiledArgs[0]})`);
429
463
  this.add("WEEK_OF_YEAR", ({ compiledArgs }) => `WEEKOFYEAR(${compiledArgs[0]})`);
430
464
  this.add("DATE_TRUNC", ({ compiledArgs }) => `DATE_TRUNC(${compiledArgs[0]}, ${compiledArgs[1]})`);
465
+ this.add("GROUP_CONCAT", (ctx) => this.renderGroupConcat(ctx));
431
466
  }
432
467
  add(name, renderer) {
433
468
  this.renderers.set(name, renderer);
@@ -435,6 +470,36 @@ var StandardFunctionStrategy = class {
435
470
  getRenderer(name) {
436
471
  return this.renderers.get(name);
437
472
  }
473
+ renderGroupConcat(ctx) {
474
+ const arg = ctx.compiledArgs[0];
475
+ const orderClause = this.buildOrderByExpression(ctx);
476
+ const orderSegment = orderClause ? ` ${orderClause}` : "";
477
+ const separatorClause = this.formatGroupConcatSeparator(ctx);
478
+ return `GROUP_CONCAT(${arg}${orderSegment}${separatorClause})`;
479
+ }
480
+ buildOrderByExpression(ctx) {
481
+ const orderBy = ctx.node.orderBy;
482
+ if (!orderBy || orderBy.length === 0) {
483
+ return "";
484
+ }
485
+ const parts = orderBy.map((order) => `${ctx.compileOperand(order.column)} ${order.direction}`);
486
+ return `ORDER BY ${parts.join(", ")}`;
487
+ }
488
+ formatGroupConcatSeparator(ctx) {
489
+ if (!ctx.node.separator) {
490
+ return "";
491
+ }
492
+ return ` SEPARATOR ${ctx.compileOperand(ctx.node.separator)}`;
493
+ }
494
+ getGroupConcatSeparatorOperand(ctx) {
495
+ return ctx.node.separator ?? _StandardFunctionStrategy.DEFAULT_GROUP_CONCAT_SEPARATOR;
496
+ }
497
+ static {
498
+ this.DEFAULT_GROUP_CONCAT_SEPARATOR = {
499
+ type: "Literal",
500
+ value: ","
501
+ };
502
+ }
438
503
  };
439
504
 
440
505
  // src/core/dialect/abstract.ts
@@ -781,7 +846,11 @@ var Dialect = class _Dialect {
781
846
  const compiledArgs = fnNode.args.map((arg) => this.compileOperand(arg, ctx));
782
847
  const renderer = this.functionStrategy.getRenderer(fnNode.name);
783
848
  if (renderer) {
784
- return renderer({ node: fnNode, compiledArgs });
849
+ return renderer({
850
+ node: fnNode,
851
+ compiledArgs,
852
+ compileOperand: (operand) => this.compileOperand(operand, ctx)
853
+ });
785
854
  }
786
855
  return `${fnNode.name}(${compiledArgs.join(", ")})`;
787
856
  }
@@ -1204,6 +1273,14 @@ var PostgresFunctionStrategy = class extends StandardFunctionStrategy {
1204
1273
  const partClean = String(partArg.value).replace(/['"]/g, "").toLowerCase();
1205
1274
  return `DATE_TRUNC('${partClean}', ${date})`;
1206
1275
  });
1276
+ this.add("GROUP_CONCAT", (ctx) => {
1277
+ const arg = ctx.compiledArgs[0];
1278
+ const orderClause = this.buildOrderByExpression(ctx);
1279
+ const orderSegment = orderClause ? ` ${orderClause}` : "";
1280
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
1281
+ const separator = ctx.compileOperand(separatorOperand);
1282
+ return `STRING_AGG(${arg}, ${separator}${orderSegment})`;
1283
+ });
1207
1284
  }
1208
1285
  };
1209
1286
 
@@ -1448,6 +1525,12 @@ var SqliteFunctionStrategy = class extends StandardFunctionStrategy {
1448
1525
  }
1449
1526
  return `date(${date}, 'start of ${partClean}')`;
1450
1527
  });
1528
+ this.add("GROUP_CONCAT", (ctx) => {
1529
+ const arg = ctx.compiledArgs[0];
1530
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
1531
+ const separator = ctx.compileOperand(separatorOperand);
1532
+ return `GROUP_CONCAT(${arg}, ${separator})`;
1533
+ });
1451
1534
  }
1452
1535
  };
1453
1536
 
@@ -1564,6 +1647,14 @@ var MssqlFunctionStrategy = class extends StandardFunctionStrategy {
1564
1647
  const partClean = String(partArg.value).replace(/['"]/g, "").toLowerCase();
1565
1648
  return `DATETRUNC(${partClean}, ${date})`;
1566
1649
  });
1650
+ this.add("GROUP_CONCAT", (ctx) => {
1651
+ const arg = ctx.compiledArgs[0];
1652
+ const separatorOperand = this.getGroupConcatSeparatorOperand(ctx);
1653
+ const separator = ctx.compileOperand(separatorOperand);
1654
+ const orderClause = this.buildOrderByExpression(ctx);
1655
+ const withinGroup = orderClause ? ` WITHIN GROUP (${orderClause})` : "";
1656
+ return `STRING_AGG(${arg}, ${separator})${withinGroup}`;
1657
+ });
1567
1658
  }
1568
1659
  };
1569
1660
 
@@ -1933,24 +2024,6 @@ var createJoinNode = (kind, tableName, condition, relationName) => ({
1933
2024
  meta: relationName ? { relationName } : void 0
1934
2025
  });
1935
2026
 
1936
- // src/core/sql/sql.ts
1937
- var JOIN_KINDS = {
1938
- /** INNER JOIN type */
1939
- INNER: "INNER",
1940
- /** LEFT JOIN type */
1941
- LEFT: "LEFT",
1942
- /** RIGHT JOIN type */
1943
- RIGHT: "RIGHT",
1944
- /** CROSS JOIN type */
1945
- CROSS: "CROSS"
1946
- };
1947
- var ORDER_DIRECTIONS = {
1948
- /** Ascending order */
1949
- ASC: "ASC",
1950
- /** Descending order */
1951
- DESC: "DESC"
1952
- };
1953
-
1954
2027
  // src/query-builder/hydration-manager.ts
1955
2028
  var HydrationManager = class _HydrationManager {
1956
2029
  /**
@@ -4059,6 +4132,21 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
4059
4132
  select(columns) {
4060
4133
  return this.clone(this.columnSelector.select(this.context, columns));
4061
4134
  }
4135
+ /**
4136
+ * Selects columns from the root table by name (typed).
4137
+ * @param cols - Column names on the root table
4138
+ */
4139
+ selectColumns(...cols) {
4140
+ const selection = {};
4141
+ for (const key of cols) {
4142
+ const col = this.env.table.columns[key];
4143
+ if (!col) {
4144
+ throw new Error(`Column '${key}' not found on table '${this.env.table.name}'`);
4145
+ }
4146
+ selection[key] = col;
4147
+ }
4148
+ return this.select(selection);
4149
+ }
4062
4150
  /**
4063
4151
 
4064
4152
  * Selects raw column expressions
@@ -4219,6 +4307,47 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
4219
4307
  nextLazy.add(relationName);
4220
4308
  return this.clone(this.context, nextLazy);
4221
4309
  }
4310
+ /**
4311
+ * Selects columns for a related table in a single hop.
4312
+ */
4313
+ selectRelationColumns(relationName, ...cols) {
4314
+ const relation = this.env.table.relations[relationName];
4315
+ if (!relation) {
4316
+ throw new Error(`Relation '${relationName}' not found on table '${this.env.table.name}'`);
4317
+ }
4318
+ const target = relation.target;
4319
+ for (const col of cols) {
4320
+ if (!target.columns[col]) {
4321
+ throw new Error(
4322
+ `Column '${col}' not found on related table '${target.name}' for relation '${relationName}'`
4323
+ );
4324
+ }
4325
+ }
4326
+ return this.include(relationName, { columns: cols });
4327
+ }
4328
+ /**
4329
+ * Convenience alias for selecting specific columns from a relation.
4330
+ */
4331
+ includePick(relationName, cols) {
4332
+ return this.selectRelationColumns(relationName, ...cols);
4333
+ }
4334
+ /**
4335
+ * Selects columns for the root table and relations from a single config object.
4336
+ */
4337
+ selectColumnsDeep(config) {
4338
+ let qb = this;
4339
+ if (config.root?.length) {
4340
+ qb = qb.selectColumns(...config.root);
4341
+ }
4342
+ for (const key of Object.keys(config)) {
4343
+ if (key === "root") continue;
4344
+ const relName = key;
4345
+ const cols = config[relName];
4346
+ if (!cols || !cols.length) continue;
4347
+ qb = qb.selectRelationColumns(relName, ...cols);
4348
+ }
4349
+ return qb;
4350
+ }
4222
4351
  getLazyRelations() {
4223
4352
  return Array.from(this.lazyRelations);
4224
4353
  }