metal-orm 1.0.51 → 1.0.53

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
@@ -74,20 +74,21 @@ MetalORM is a TypeScript-first, AST-driven SQL toolkit you can dial up or down d
74
74
  <a id="documentation"></a>
75
75
  ## Documentation 📚
76
76
 
77
- Full docs live in the `docs/` folder:
78
-
79
- - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
80
- - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
81
- - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
82
- - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
83
- - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
84
- - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
85
- - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
86
- - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
87
- - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
88
- - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
89
- - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
90
- - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
77
+ Full docs live in the `docs/` folder:
78
+
79
+ - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
80
+ - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
81
+ - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
82
+ - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
83
+ - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
84
+ - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
85
+ - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
86
+ - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
87
+ - [Save Graph](https://github.com/celsowm/metal-orm/blob/main/docs/save-graph.md)
88
+ - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
89
+ - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
90
+ - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
91
+ - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
91
92
  - [DB ➜ TS Type Mapping](https://github.com/celsowm/metal-orm/blob/main/docs/db-to-ts-types.md)
92
93
 
93
94
  ---
@@ -249,7 +250,9 @@ console.log(rows);
249
250
  // ]
250
251
  ```
251
252
 
252
- That’s it: schema, query, SQL, done.
253
+ That’s it: schema, query, SQL, done.
254
+
255
+ If you are using the Level 2 runtime (`OrmSession`), `SelectQueryBuilder` also provides `count(session)` and `executePaged(session, { page, pageSize })` for common pagination patterns.
253
256
 
254
257
  #### Column pickers (preferred selection helpers)
255
258
 
package/dist/index.cjs CHANGED
@@ -101,6 +101,7 @@ __export(index_exports, {
101
101
  cos: () => cos,
102
102
  cot: () => cot,
103
103
  count: () => count,
104
+ countAll: () => countAll,
104
105
  createEntityFromRow: () => createEntityFromRow,
105
106
  createEntityProxy: () => createEntityProxy,
106
107
  createExecutorFromQueryRunner: () => createExecutorFromQueryRunner,
@@ -163,6 +164,7 @@ __export(index_exports, {
163
164
  isValueOperandInput: () => isValueOperandInput,
164
165
  isWindowFunctionNode: () => isWindowFunctionNode,
165
166
  jsonPath: () => jsonPath,
167
+ jsonify: () => jsonify,
166
168
  lag: () => lag,
167
169
  lastValue: () => lastValue,
168
170
  lead: () => lead,
@@ -873,6 +875,11 @@ var sum = buildAggregate("SUM");
873
875
  var avg = buildAggregate("AVG");
874
876
  var min = buildAggregate("MIN");
875
877
  var max = buildAggregate("MAX");
878
+ var countAll = () => ({
879
+ type: "Function",
880
+ name: "COUNT",
881
+ args: []
882
+ });
876
883
  var toOrderByNode = (order) => ({
877
884
  type: "OrderBy",
878
885
  term: columnOperand(order.column),
@@ -1075,7 +1082,7 @@ var StandardFunctionStrategy = class _StandardFunctionStrategy {
1075
1082
  this.registerStandard();
1076
1083
  }
1077
1084
  registerStandard() {
1078
- this.add("COUNT", ({ compiledArgs }) => `COUNT(${compiledArgs.join(", ")})`);
1085
+ this.add("COUNT", ({ compiledArgs }) => compiledArgs.length ? `COUNT(${compiledArgs.join(", ")})` : "COUNT(*)");
1079
1086
  this.add("SUM", ({ compiledArgs }) => `SUM(${compiledArgs[0]})`);
1080
1087
  this.add("AVG", ({ compiledArgs }) => `AVG(${compiledArgs[0]})`);
1081
1088
  this.add("MIN", ({ compiledArgs }) => `MIN(${compiledArgs[0]})`);
@@ -5001,10 +5008,10 @@ var flattenResults = (results) => {
5001
5008
  }
5002
5009
  return rows;
5003
5010
  };
5004
- var executeWithEntityContext = async (entityCtx, qb) => {
5011
+ var executeWithContexts = async (execCtx, entityCtx, qb) => {
5005
5012
  const ast = qb.getAST();
5006
- const compiled = entityCtx.dialect.compileSelect(ast);
5007
- const executed = await entityCtx.executor.executeSql(compiled.sql, compiled.params);
5013
+ const compiled = execCtx.dialect.compileSelect(ast);
5014
+ const executed = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
5008
5015
  const rows = flattenResults(executed);
5009
5016
  if (ast.setOps && ast.setOps.length > 0) {
5010
5017
  return rows.map((row) => createEntityProxy(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
@@ -5013,14 +5020,14 @@ var executeWithEntityContext = async (entityCtx, qb) => {
5013
5020
  return hydrated.map((row) => createEntityFromRow(entityCtx, qb.getTable(), row, qb.getLazyRelations()));
5014
5021
  };
5015
5022
  async function executeHydrated(session, qb) {
5016
- return executeWithEntityContext(session, qb);
5023
+ return executeWithContexts(session.getExecutionContext(), session, qb);
5017
5024
  }
5018
- async function executeHydratedWithContexts(_execCtx, hydCtx, qb) {
5025
+ async function executeHydratedWithContexts(execCtx, hydCtx, qb) {
5019
5026
  const entityCtx = hydCtx.entityContext;
5020
5027
  if (!entityCtx) {
5021
5028
  throw new Error("Hydration context is missing an EntityContext");
5022
5029
  }
5023
- return executeWithEntityContext(entityCtx, qb);
5030
+ return executeWithContexts(execCtx, entityCtx, qb);
5024
5031
  }
5025
5032
 
5026
5033
  // src/query-builder/query-resolution.ts
@@ -5374,6 +5381,52 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
5374
5381
  async execute(ctx) {
5375
5382
  return executeHydrated(ctx, this);
5376
5383
  }
5384
+ withAst(ast) {
5385
+ const nextState = new SelectQueryState(this.env.table, ast);
5386
+ const nextContext = {
5387
+ ...this.context,
5388
+ state: nextState
5389
+ };
5390
+ return this.clone(nextContext);
5391
+ }
5392
+ async count(session) {
5393
+ const unpagedAst = {
5394
+ ...this.context.state.ast,
5395
+ orderBy: void 0,
5396
+ limit: void 0,
5397
+ offset: void 0
5398
+ };
5399
+ const subAst = this.withAst(unpagedAst).getAST();
5400
+ const countQuery = {
5401
+ type: "SelectQuery",
5402
+ from: derivedTable(subAst, "__metal_count"),
5403
+ columns: [{ type: "Function", name: "COUNT", args: [], alias: "total" }],
5404
+ joins: []
5405
+ };
5406
+ const execCtx = session.getExecutionContext();
5407
+ const compiled = execCtx.dialect.compileSelect(countQuery);
5408
+ const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
5409
+ const value = results[0]?.values?.[0]?.[0];
5410
+ if (typeof value === "number") return value;
5411
+ if (typeof value === "bigint") return Number(value);
5412
+ if (typeof value === "string") return Number(value);
5413
+ return value === null || value === void 0 ? 0 : Number(value);
5414
+ }
5415
+ async executePaged(session, options) {
5416
+ const { page, pageSize } = options;
5417
+ if (!Number.isInteger(page) || page < 1) {
5418
+ throw new Error("executePaged: page must be an integer >= 1");
5419
+ }
5420
+ if (!Number.isInteger(pageSize) || pageSize < 1) {
5421
+ throw new Error("executePaged: pageSize must be an integer >= 1");
5422
+ }
5423
+ const offset = (page - 1) * pageSize;
5424
+ const [items, totalItems] = await Promise.all([
5425
+ this.limit(pageSize).offset(offset).execute(session),
5426
+ this.count(session)
5427
+ ]);
5428
+ return { items, totalItems };
5429
+ }
5377
5430
  /**
5378
5431
  * Executes the query with provided execution and hydration contexts
5379
5432
  * @param execCtx - Execution context
@@ -6223,8 +6276,9 @@ var UpdateQueryBuilder = class _UpdateQueryBuilder {
6223
6276
  * @returns A promise that resolves to the query results
6224
6277
  */
6225
6278
  async execute(session) {
6226
- const compiled = this.compile(session.dialect);
6227
- return session.executor.executeSql(compiled.sql, compiled.params);
6279
+ const execCtx = session.getExecutionContext();
6280
+ const compiled = this.compile(execCtx.dialect);
6281
+ return execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
6228
6282
  }
6229
6283
  /**
6230
6284
  * Returns the Abstract Syntax Tree (AST) representation of the query
@@ -6408,8 +6462,9 @@ var DeleteQueryBuilder = class _DeleteQueryBuilder {
6408
6462
  * @returns A promise that resolves to the query results
6409
6463
  */
6410
6464
  async execute(session) {
6411
- const compiled = this.compile(session.dialect);
6412
- return session.executor.executeSql(compiled.sql, compiled.params);
6465
+ const execCtx = session.getExecutionContext();
6466
+ const compiled = this.compile(execCtx.dialect);
6467
+ return execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
6413
6468
  }
6414
6469
  /**
6415
6470
  * Returns the Abstract Syntax Tree (AST) representation of the query
@@ -9521,18 +9576,30 @@ var runInTransaction = async (executor, action) => {
9521
9576
 
9522
9577
  // src/orm/save-graph.ts
9523
9578
  var toKey8 = (value) => value === null || value === void 0 ? "" : String(value);
9524
- var pickColumns = (table, payload) => {
9579
+ var coerceColumnValue = (table, columnName, value, options) => {
9580
+ if (options.coerce !== "json") return value;
9581
+ if (value === null || value === void 0) return value;
9582
+ const column = table.columns[columnName];
9583
+ if (!column) return value;
9584
+ const normalized = normalizeColumnType(column.type);
9585
+ const isDateLikeColumn = normalized === "date" || normalized === "datetime" || normalized === "timestamp" || normalized === "timestamptz";
9586
+ if (isDateLikeColumn && value instanceof Date) {
9587
+ return value.toISOString();
9588
+ }
9589
+ return value;
9590
+ };
9591
+ var pickColumns = (table, payload, options) => {
9525
9592
  const columns = {};
9526
9593
  for (const key of Object.keys(table.columns)) {
9527
9594
  if (payload[key] !== void 0) {
9528
- columns[key] = payload[key];
9595
+ columns[key] = coerceColumnValue(table, key, payload[key], options);
9529
9596
  }
9530
9597
  }
9531
9598
  return columns;
9532
9599
  };
9533
- var ensureEntity = (session, table, payload) => {
9600
+ var ensureEntity = (session, table, payload, options) => {
9534
9601
  const pk = findPrimaryKey(table);
9535
- const row = pickColumns(table, payload);
9602
+ const row = pickColumns(table, payload, options);
9536
9603
  const pkValue = payload[pk];
9537
9604
  if (pkValue !== void 0 && pkValue !== null) {
9538
9605
  const tracked = session.getEntity(table, pkValue);
@@ -9545,10 +9612,10 @@ var ensureEntity = (session, table, payload) => {
9545
9612
  }
9546
9613
  return createEntityFromRow(session, table, row);
9547
9614
  };
9548
- var assignColumns = (table, entity, payload) => {
9615
+ var assignColumns = (table, entity, payload, options) => {
9549
9616
  for (const key of Object.keys(table.columns)) {
9550
9617
  if (payload[key] !== void 0) {
9551
- entity[key] = payload[key];
9618
+ entity[key] = coerceColumnValue(table, key, payload[key], options);
9552
9619
  }
9553
9620
  }
9554
9621
  };
@@ -9575,8 +9642,8 @@ var handleHasMany = async (session, root, relationName, relation, payload, optio
9575
9642
  const asObj = typeof item === "object" ? item : { [targetPk]: item };
9576
9643
  const pkValue = asObj[targetPk];
9577
9644
  const current = findInCollectionByPk(existing, targetPk, pkValue) ?? (pkValue !== void 0 && pkValue !== null ? session.getEntity(targetTable, pkValue) : void 0);
9578
- const entity = current ?? ensureEntity(session, targetTable, asObj);
9579
- assignColumns(targetTable, entity, asObj);
9645
+ const entity = current ?? ensureEntity(session, targetTable, asObj, options);
9646
+ assignColumns(targetTable, entity, asObj, options);
9580
9647
  await applyGraphToEntity(session, targetTable, entity, asObj, options);
9581
9648
  if (!isEntityInCollection(collection.getItems(), targetPk, entity)) {
9582
9649
  collection.attach(entity);
@@ -9651,8 +9718,8 @@ var handleBelongsToMany = async (session, root, relationName, relation, payload,
9651
9718
  }
9652
9719
  const asObj = item;
9653
9720
  const pkValue = asObj[targetPk];
9654
- const entity = pkValue !== void 0 && pkValue !== null ? session.getEntity(targetTable, pkValue) ?? ensureEntity(session, targetTable, asObj) : ensureEntity(session, targetTable, asObj);
9655
- assignColumns(targetTable, entity, asObj);
9721
+ const entity = pkValue !== void 0 && pkValue !== null ? session.getEntity(targetTable, pkValue) ?? ensureEntity(session, targetTable, asObj, options) : ensureEntity(session, targetTable, asObj, options);
9722
+ assignColumns(targetTable, entity, asObj, options);
9656
9723
  await applyGraphToEntity(session, targetTable, entity, asObj, options);
9657
9724
  if (!isEntityInCollection(collection.getItems(), targetPk, entity)) {
9658
9725
  collection.attach(entity);
@@ -9683,7 +9750,7 @@ var applyRelation = async (session, table, entity, relationName, relation, paylo
9683
9750
  }
9684
9751
  };
9685
9752
  var applyGraphToEntity = async (session, table, entity, payload, options) => {
9686
- assignColumns(table, entity, payload);
9753
+ assignColumns(table, entity, payload, options);
9687
9754
  for (const [relationName, relation] of Object.entries(table.relations)) {
9688
9755
  if (!(relationName in payload)) continue;
9689
9756
  await applyRelation(session, table, entity, relationName, relation, payload[relationName], options);
@@ -9694,7 +9761,7 @@ var saveGraphInternal = async (session, entityClass, payload, options = {}) => {
9694
9761
  if (!table) {
9695
9762
  throw new Error("Entity metadata has not been bootstrapped");
9696
9763
  }
9697
- const root = ensureEntity(session, table, payload);
9764
+ const root = ensureEntity(session, table, payload, options);
9698
9765
  await applyGraphToEntity(session, table, root, payload, options);
9699
9766
  return root;
9700
9767
  };
@@ -9877,13 +9944,6 @@ var OrmSession = class {
9877
9944
  async findMany(qb) {
9878
9945
  return executeHydrated(this, qb);
9879
9946
  }
9880
- /**
9881
- * Saves an entity graph (root + nested relations) based on a DTO-like payload.
9882
- * @param entityClass - Root entity constructor
9883
- * @param payload - DTO payload containing column values and nested relations
9884
- * @param options - Graph save options
9885
- * @returns The root entity instance
9886
- */
9887
9947
  async saveGraph(entityClass, payload, options) {
9888
9948
  const { transactional = true, ...graphOptions } = options ?? {};
9889
9949
  const execute = () => saveGraphInternal(this, entityClass, payload, graphOptions);
@@ -10084,6 +10144,17 @@ var Orm = class {
10084
10144
  }
10085
10145
  };
10086
10146
 
10147
+ // src/orm/jsonify.ts
10148
+ var jsonify = (value) => {
10149
+ const record = value;
10150
+ const result = {};
10151
+ for (const key of Object.keys(record)) {
10152
+ const entry = record[key];
10153
+ result[key] = entry instanceof Date ? entry.toISOString() : entry;
10154
+ }
10155
+ return result;
10156
+ };
10157
+
10087
10158
  // src/decorators/decorator-metadata.ts
10088
10159
  var METADATA_KEY = "metal-orm:decorators";
10089
10160
  var isStandardDecoratorContext = (value) => {
@@ -10906,6 +10977,7 @@ function createPooledExecutorFactory(opts) {
10906
10977
  cos,
10907
10978
  cot,
10908
10979
  count,
10980
+ countAll,
10909
10981
  createEntityFromRow,
10910
10982
  createEntityProxy,
10911
10983
  createExecutorFromQueryRunner,
@@ -10968,6 +11040,7 @@ function createPooledExecutorFactory(opts) {
10968
11040
  isValueOperandInput,
10969
11041
  isWindowFunctionNode,
10970
11042
  jsonPath,
11043
+ jsonify,
10971
11044
  lag,
10972
11045
  lastValue,
10973
11046
  lead,