linkgress-orm 0.1.18 → 0.1.20

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 (36) hide show
  1. package/dist/database/database-client.interface.d.ts +29 -0
  2. package/dist/database/database-client.interface.d.ts.map +1 -1
  3. package/dist/database/database-client.interface.js +45 -1
  4. package/dist/database/database-client.interface.js.map +1 -1
  5. package/dist/database/pg-client.d.ts +5 -0
  6. package/dist/database/pg-client.d.ts.map +1 -1
  7. package/dist/database/pg-client.js +27 -0
  8. package/dist/database/pg-client.js.map +1 -1
  9. package/dist/database/postgres-client.d.ts +6 -0
  10. package/dist/database/postgres-client.d.ts.map +1 -1
  11. package/dist/database/postgres-client.js +17 -0
  12. package/dist/database/postgres-client.js.map +1 -1
  13. package/dist/entity/db-column.d.ts +17 -0
  14. package/dist/entity/db-column.d.ts.map +1 -1
  15. package/dist/entity/db-column.js.map +1 -1
  16. package/dist/entity/db-context.d.ts +56 -6
  17. package/dist/entity/db-context.d.ts.map +1 -1
  18. package/dist/entity/db-context.js +180 -74
  19. package/dist/entity/db-context.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/query/collection-strategy.interface.d.ts +12 -0
  24. package/dist/query/collection-strategy.interface.d.ts.map +1 -1
  25. package/dist/query/query-builder.d.ts +15 -2
  26. package/dist/query/query-builder.d.ts.map +1 -1
  27. package/dist/query/query-builder.js +179 -48
  28. package/dist/query/query-builder.js.map +1 -1
  29. package/dist/query/sql-utils.d.ts +2 -0
  30. package/dist/query/sql-utils.d.ts.map +1 -1
  31. package/dist/query/sql-utils.js +24 -7
  32. package/dist/query/sql-utils.js.map +1 -1
  33. package/dist/query/strategies/temptable-collection-strategy.d.ts.map +1 -1
  34. package/dist/query/strategies/temptable-collection-strategy.js +4 -3
  35. package/dist/query/strategies/temptable-collection-strategy.js.map +1 -1
  36. package/package.json +2 -3
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DatabaseContext = exports.DbEntityTable = exports.EntityInsertBuilder = exports.DataContext = exports.TableAccessor = exports.InsertBuilder = exports.QueryExecutor = exports.TimeTracer = void 0;
4
+ const database_client_interface_1 = require("../database/database-client.interface");
4
5
  const entity_base_1 = require("./entity-base");
5
6
  const model_config_1 = require("./model-config");
6
7
  const query_builder_1 = require("../query/query-builder");
@@ -528,15 +529,21 @@ class TableAccessor {
528
529
  const column = this.schema.columns[key];
529
530
  if (column) {
530
531
  const config = column.build();
531
- if (!config.autoIncrement) {
532
- columns.push(`"${config.name}"`);
533
- // Apply toDriver mapper if present
534
- const mappedValue = config.mapper
535
- ? config.mapper.toDriver(value)
536
- : value;
537
- values.push(mappedValue);
538
- placeholders.push(`$${paramIndex++}`);
532
+ // Skip auto-increment columns
533
+ if (config.autoIncrement) {
534
+ continue;
535
+ }
536
+ // Skip columns with undefined values if they have a default - let the DB use the default
537
+ if (value === undefined && (config.default !== undefined || config.identity)) {
538
+ continue;
539
539
  }
540
+ columns.push(`"${config.name}"`);
541
+ // Apply toDriver mapper if present
542
+ const mappedValue = config.mapper
543
+ ? config.mapper.toDriver(value)
544
+ : value;
545
+ values.push(mappedValue);
546
+ placeholders.push(`$${paramIndex++}`);
540
547
  }
541
548
  }
542
549
  const returningColumns = (0, sql_utils_1.buildReturningColumnList)(this.schema);
@@ -792,13 +799,18 @@ class DataContext {
792
799
  }
793
800
  /**
794
801
  * Get table accessor by name
802
+ * When in a transaction, creates a fresh accessor with the transactional client
795
803
  */
796
804
  getTable(name) {
797
- const accessor = this.tableAccessors.get(name);
798
- if (!accessor) {
805
+ const cachedAccessor = this.tableAccessors.get(name);
806
+ if (!cachedAccessor) {
799
807
  throw new Error(`Table ${String(name)} not found in schema`);
800
808
  }
801
- return accessor;
809
+ // If in a transaction, create a new accessor with the current (transactional) client
810
+ if (this.client.isInTransaction()) {
811
+ return new TableAccessor(cachedAccessor.tableBuilder, this.client, this.schemaRegistry, this.executor, this.queryOptions?.collectionStrategy);
812
+ }
813
+ return cachedAccessor;
802
814
  }
803
815
  /**
804
816
  * Execute raw SQL query with optional type parameter for results
@@ -821,22 +833,25 @@ class DataContext {
821
833
  }
822
834
  /**
823
835
  * Execute in transaction
836
+ * Uses the database client's native transaction support for proper handling
837
+ * across different drivers (pg uses BEGIN/COMMIT, postgres uses sql.begin())
824
838
  */
825
839
  async transaction(fn) {
826
- const connection = await this.client.connect();
827
- try {
828
- await connection.query('BEGIN');
829
- const result = await fn(this);
830
- await connection.query('COMMIT');
831
- return result;
832
- }
833
- catch (error) {
834
- await connection.query('ROLLBACK');
835
- throw error;
836
- }
837
- finally {
838
- connection.release();
839
- }
840
+ // Store the original client
841
+ const originalClient = this.client;
842
+ return await this.client.transaction(async (queryFn) => {
843
+ // Create a transactional client that routes all queries through the transaction
844
+ const txClient = new database_client_interface_1.TransactionalClient(queryFn, originalClient);
845
+ // Temporarily swap the client to use the transactional one
846
+ this.client = txClient;
847
+ try {
848
+ return await fn(this);
849
+ }
850
+ finally {
851
+ // Restore the original client
852
+ this.client = originalClient;
853
+ }
854
+ });
840
855
  }
841
856
  /**
842
857
  * Get schema manager for create/drop operations and automatic migrations
@@ -977,6 +992,36 @@ class DbEntityTable {
977
992
  }
978
993
  return columns;
979
994
  }
995
+ /**
996
+ * Get an array of all column property names (keys) for this entity.
997
+ * Returns a strongly typed array where each element is a valid column key of TEntity.
998
+ *
999
+ * This is a convenience method equivalent to `getColumns().map(c => c.propertyName)`
1000
+ * but with better type inference.
1001
+ *
1002
+ * @returns Array of column property names typed as ExtractDbColumnKeys<TEntity>
1003
+ *
1004
+ * @example
1005
+ * ```typescript
1006
+ * // Get all column keys
1007
+ * const keys = db.users.getColumnKeys();
1008
+ * // Type: ExtractDbColumnKeys<User>[] which is ('id' | 'username' | 'email' | ...)[]
1009
+ *
1010
+ * // Use for dynamic property access
1011
+ * const user = await db.users.findOne(u => eq(u.id, 1));
1012
+ * for (const key of db.users.getColumnKeys()) {
1013
+ * console.log(`${key}: ${user[key]}`); // TypeScript knows key is valid
1014
+ * }
1015
+ *
1016
+ * // Use for building dynamic queries
1017
+ * const columnKeys = db.users.getColumnKeys();
1018
+ * // columnKeys[0] is typed as 'id' | 'username' | 'email' | ...
1019
+ * ```
1020
+ */
1021
+ getColumnKeys() {
1022
+ const schema = this._getSchema();
1023
+ return Object.keys(schema.columns);
1024
+ }
980
1025
  /**
981
1026
  * Get an object containing all entity properties as DbColumn references.
982
1027
  * Useful for building dynamic queries or accessing column metadata.
@@ -1225,21 +1270,20 @@ class DbEntityTable {
1225
1270
  */
1226
1271
  where(condition) {
1227
1272
  const schema = this._getSchema();
1228
- // Create a selector that selects all columns with navigation property access
1273
+ // Create a selector that selects all columns only (not navigation properties)
1229
1274
  const allColumnsSelector = (e) => {
1230
1275
  const result = {};
1231
1276
  // Copy all column properties
1232
1277
  for (const colName of Object.keys(schema.columns)) {
1233
1278
  result[colName] = e[colName];
1234
1279
  }
1235
- // Add navigation properties as enumerable getters
1236
- // The getter returns the actual navigation builder (CollectionQueryBuilder/ReferenceQueryBuilder)
1237
- // which allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1238
- // When the selection is analyzed, the builder is detected and handled appropriately
1280
+ // Add navigation properties as non-enumerable getters
1281
+ // This allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1282
+ // but they won't be included in the default query output
1239
1283
  for (const relName of Object.keys(schema.relations)) {
1240
1284
  Object.defineProperty(result, relName, {
1241
1285
  get: () => e[relName],
1242
- enumerable: true, // Enumerable so it's included in Object.entries
1286
+ enumerable: false, // Non-enumerable so it's NOT included in default selection
1243
1287
  configurable: true,
1244
1288
  });
1245
1289
  }
@@ -1255,21 +1299,20 @@ class DbEntityTable {
1255
1299
  */
1256
1300
  with(...ctes) {
1257
1301
  const schema = this._getSchema();
1258
- // Create a selector that selects all columns with navigation property access
1302
+ // Create a selector that selects all columns only (not navigation properties)
1259
1303
  const allColumnsSelector = (e) => {
1260
1304
  const result = {};
1261
1305
  // Copy all column properties
1262
1306
  for (const colName of Object.keys(schema.columns)) {
1263
1307
  result[colName] = e[colName];
1264
1308
  }
1265
- // Add navigation properties as enumerable getters
1266
- // The getter returns the actual navigation builder (CollectionQueryBuilder/ReferenceQueryBuilder)
1267
- // which allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1268
- // When the selection is analyzed, the builder is detected and handled appropriately
1309
+ // Add navigation properties as non-enumerable getters
1310
+ // This allows chained selectors like .select(u => ({ posts: u.posts!.where(...) }))
1311
+ // but they won't be included in the default query output
1269
1312
  for (const relName of Object.keys(schema.relations)) {
1270
1313
  Object.defineProperty(result, relName, {
1271
1314
  get: () => e[relName],
1272
- enumerable: true, // Enumerable so it's included in Object.entries
1315
+ enumerable: false, // Non-enumerable so it's NOT included in default selection
1273
1316
  configurable: true,
1274
1317
  });
1275
1318
  }
@@ -1411,20 +1454,37 @@ class DbEntityTable {
1411
1454
  const executor = this._getExecutor();
1412
1455
  const client = this._getClient();
1413
1456
  const qualifiedTableName = this._getQualifiedTableName();
1414
- // Get columns from first row
1415
- const referenceItem = data[0];
1457
+ // Get columns from all rows - a column is included if ANY row has a non-undefined value for it
1416
1458
  const columnConfigs = [];
1417
1459
  for (const [propName, colBuilder] of Object.entries(schema.columns)) {
1418
- if (propName in referenceItem) {
1419
- const config = colBuilder.build();
1420
- if (!config.autoIncrement || overridingSystemValue) {
1421
- columnConfigs.push({
1422
- propName,
1423
- dbName: config.name,
1424
- mapper: config.mapper,
1425
- });
1460
+ const config = colBuilder.build();
1461
+ // Skip auto-increment columns (unless overriding)
1462
+ if (config.autoIncrement && !overridingSystemValue) {
1463
+ continue;
1464
+ }
1465
+ // Check if any row has a defined (non-undefined) value for this column
1466
+ const hasDefinedValue = data.some(record => {
1467
+ const value = record[propName];
1468
+ return value !== undefined;
1469
+ });
1470
+ // If column has a default and ALL rows have undefined, skip it (let DB use default)
1471
+ // Otherwise, include it if at least one row has a defined value
1472
+ if (!hasDefinedValue) {
1473
+ // All rows have undefined - if there's a default, skip the column
1474
+ if (config.default !== undefined || config.identity) {
1475
+ continue;
1476
+ }
1477
+ // No default, but column is present with undefined in data - include it (will become NULL)
1478
+ const isPresentInAnyRow = data.some(record => propName in record);
1479
+ if (!isPresentInAnyRow) {
1480
+ continue;
1426
1481
  }
1427
1482
  }
1483
+ columnConfigs.push({
1484
+ propName,
1485
+ dbName: config.name,
1486
+ mapper: config.mapper,
1487
+ });
1428
1488
  }
1429
1489
  // Build VALUES clauses
1430
1490
  const valuesClauses = [];
@@ -1461,7 +1521,7 @@ class DbEntityTable {
1461
1521
  if (!returningClause) {
1462
1522
  return undefined;
1463
1523
  }
1464
- return this.mapReturningResults(result.rows, returning);
1524
+ return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
1465
1525
  }
1466
1526
  /**
1467
1527
  * Upsert with advanced configuration
@@ -1669,7 +1729,7 @@ class DbEntityTable {
1669
1729
  if (!returningClause) {
1670
1730
  return undefined;
1671
1731
  }
1672
- return this.mapReturningResults(result.rows, returning);
1732
+ return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
1673
1733
  }
1674
1734
  /**
1675
1735
  * Map database column names back to property names
@@ -1752,8 +1812,10 @@ class DbEntityTable {
1752
1812
  throw new Error('No valid columns to update');
1753
1813
  }
1754
1814
  // No WHERE clause - updates all records
1755
- // Build RETURNING clause
1756
- const returningClause = table.buildReturningClause(returning);
1815
+ // Build RETURNING clause (not needed for count-only)
1816
+ const returningClause = returning !== 'count'
1817
+ ? table.buildReturningClause(returning)
1818
+ : undefined;
1757
1819
  const qualifiedTableName = table._getQualifiedTableName();
1758
1820
  let sql = `UPDATE ${qualifiedTableName} SET ${setClauses.join(', ')}`;
1759
1821
  if (returningClause) {
@@ -1762,15 +1824,26 @@ class DbEntityTable {
1762
1824
  const result = executor
1763
1825
  ? await executor.query(sql, values)
1764
1826
  : await client.query(sql, values);
1827
+ // Return affected count
1828
+ if (returning === 'count') {
1829
+ return result.rowCount ?? 0;
1830
+ }
1765
1831
  if (!returningClause) {
1766
1832
  return undefined;
1767
1833
  }
1768
- return table.mapReturningResults(result.rows, returning);
1834
+ return table.mapReturningResults(result.rows, returningClause.aliasToProperty);
1769
1835
  };
1770
1836
  return {
1771
1837
  then(onfulfilled, onrejected) {
1772
1838
  return executeUpdate(undefined).then(onfulfilled, onrejected);
1773
1839
  },
1840
+ affectedCount() {
1841
+ return {
1842
+ then(onfulfilled, onrejected) {
1843
+ return executeUpdate('count').then(onfulfilled, onrejected);
1844
+ }
1845
+ };
1846
+ },
1774
1847
  returning(selector) {
1775
1848
  const returningConfig = selector ?? true;
1776
1849
  return {
@@ -1971,7 +2044,7 @@ WHERE ${whereClause}`.trim();
1971
2044
  if (!returningClause) {
1972
2045
  return undefined;
1973
2046
  }
1974
- return this.mapReturningResults(result.rows, returning);
2047
+ return this.mapReturningResults(result.rows, returningClause.aliasToProperty);
1975
2048
  }
1976
2049
  /**
1977
2050
  * Delete all records from the table
@@ -1991,11 +2064,14 @@ WHERE ${whereClause}`.trim();
1991
2064
  const baseSql = `DELETE FROM ${qualifiedTableName}`;
1992
2065
  const table = this;
1993
2066
  const executeDelete = async (returningConfig) => {
1994
- if (!returningConfig) {
2067
+ if (!returningConfig || returningConfig === 'count') {
1995
2068
  const sql = baseSql;
1996
2069
  const result = executor
1997
2070
  ? await executor.query(sql, [])
1998
2071
  : await client.query(sql, []);
2072
+ if (returningConfig === 'count') {
2073
+ return result.rowCount ?? 0;
2074
+ }
1999
2075
  return;
2000
2076
  }
2001
2077
  // Build RETURNING clause
@@ -2005,13 +2081,20 @@ WHERE ${whereClause}`.trim();
2005
2081
  ? await executor.query(sql, [])
2006
2082
  : await client.query(sql, []);
2007
2083
  if (returningClause && result.rows) {
2008
- return table.mapReturningResults(result.rows, returningConfig);
2084
+ return table.mapReturningResults(result.rows, returningClause.aliasToProperty);
2009
2085
  }
2010
2086
  };
2011
2087
  const fluent = {
2012
2088
  then: (resolve, reject) => {
2013
2089
  return executeDelete().then(resolve, reject);
2014
2090
  },
2091
+ affectedCount: () => {
2092
+ return {
2093
+ then: (resolve, reject) => {
2094
+ return executeDelete('count').then(resolve, reject);
2095
+ }
2096
+ };
2097
+ },
2015
2098
  returning: ((selector) => {
2016
2099
  const returningConfig = selector ?? true;
2017
2100
  return {
@@ -2090,14 +2173,20 @@ WHERE ${whereClause}`.trim();
2090
2173
  if (typeof selection === 'object' && selection !== null) {
2091
2174
  const columns = [];
2092
2175
  const sqlParts = [];
2176
+ const aliasToProperty = new Map();
2093
2177
  for (const [alias, field] of Object.entries(selection)) {
2094
2178
  if (field && typeof field === 'object' && '__dbColumnName' in field) {
2095
2179
  const dbName = field.__dbColumnName;
2180
+ const propName = field.__fieldName; // Property name on entity
2096
2181
  columns.push(alias);
2097
2182
  sqlParts.push(`${prefix}"${dbName}" AS "${alias}"`);
2183
+ // Track alias -> property name mapping for mapper lookup
2184
+ if (propName) {
2185
+ aliasToProperty.set(alias, propName);
2186
+ }
2098
2187
  }
2099
2188
  }
2100
- return { sql: sqlParts.join(', '), columns };
2189
+ return { sql: sqlParts.join(', '), columns, aliasToProperty };
2101
2190
  }
2102
2191
  // Single field selection
2103
2192
  if (selection && typeof selection === 'object' && '__dbColumnName' in selection) {
@@ -2107,33 +2196,50 @@ WHERE ${whereClause}`.trim();
2107
2196
  return null;
2108
2197
  }
2109
2198
  /**
2110
- * Map row results based on returning config
2199
+ * Map row results applying custom mappers
2111
2200
  * @internal
2112
- */
2113
- mapReturningResults(rows, returning) {
2114
- if (returning === true) {
2115
- // Full entity mapping
2201
+ * @param rows - Raw database rows
2202
+ * @param aliasToProperty - Optional mapping from result aliases to entity property names.
2203
+ * If undefined, assumes full entity mapping (db column names -> property names)
2204
+ */
2205
+ mapReturningResults(rows, aliasToProperty) {
2206
+ // If no alias mapping provided, use full entity mapping
2207
+ // This handles the `returning === true` case where we want proper db column -> property name mapping
2208
+ if (!aliasToProperty || aliasToProperty.size === 0) {
2116
2209
  return this.mapResultsToEntities(rows);
2117
2210
  }
2118
- // For selector functions, rows are already in the correct shape
2119
- // Just apply any type mappers needed
2120
2211
  const schema = this._getSchema();
2121
2212
  return rows.map(row => {
2122
2213
  const mapped = {};
2123
2214
  for (const [key, value] of Object.entries(row)) {
2124
- // Try to find column by alias or name
2125
- const colEntry = Object.entries(schema.columns).find(([propName, col]) => {
2126
- const config = col.build();
2127
- return propName === key || config.name === key;
2128
- });
2129
- if (colEntry) {
2130
- const [propName, col] = colEntry;
2131
- const config = col.build();
2132
- // Apply fromDriver mapper if present
2133
- mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
2215
+ // Check if this key is an alias that maps to a property name
2216
+ const propName = aliasToProperty.get(key);
2217
+ if (propName) {
2218
+ // Found via alias mapping - use the property name to look up the column
2219
+ const colBuilder = schema.columns[propName];
2220
+ if (colBuilder) {
2221
+ const config = colBuilder.build();
2222
+ // Apply fromDriver mapper if present
2223
+ mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
2224
+ }
2225
+ else {
2226
+ mapped[key] = value;
2227
+ }
2134
2228
  }
2135
2229
  else {
2136
- mapped[key] = value;
2230
+ // Try to find column by direct property name or db column name match
2231
+ const colEntry = Object.entries(schema.columns).find(([pName, col]) => {
2232
+ const config = col.build();
2233
+ return pName === key || config.name === key;
2234
+ });
2235
+ if (colEntry) {
2236
+ const config = colEntry[1].build();
2237
+ // Apply fromDriver mapper if present
2238
+ mapped[key] = config.mapper ? config.mapper.fromDriver(value) : value;
2239
+ }
2240
+ else {
2241
+ mapped[key] = value;
2242
+ }
2137
2243
  }
2138
2244
  }
2139
2245
  return mapped;