metal-orm 1.1.3 → 1.1.5

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 (38) hide show
  1. package/README.md +715 -703
  2. package/dist/index.cjs +655 -75
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +170 -8
  5. package/dist/index.d.ts +170 -8
  6. package/dist/index.js +649 -75
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/scripts/generate-entities/render.mjs +24 -1
  10. package/scripts/naming-strategy.mjs +16 -1
  11. package/src/core/ast/procedure.ts +21 -0
  12. package/src/core/ast/query.ts +47 -19
  13. package/src/core/ddl/introspect/utils.ts +56 -56
  14. package/src/core/dialect/abstract.ts +560 -547
  15. package/src/core/dialect/base/sql-dialect.ts +43 -29
  16. package/src/core/dialect/mssql/index.ts +369 -232
  17. package/src/core/dialect/mysql/index.ts +99 -7
  18. package/src/core/dialect/postgres/index.ts +121 -60
  19. package/src/core/dialect/sqlite/index.ts +97 -64
  20. package/src/core/execution/db-executor.ts +108 -90
  21. package/src/core/execution/executors/mssql-executor.ts +28 -24
  22. package/src/core/execution/executors/mysql-executor.ts +62 -27
  23. package/src/core/execution/executors/sqlite-executor.ts +10 -9
  24. package/src/index.ts +9 -6
  25. package/src/orm/execute-procedure.ts +77 -0
  26. package/src/orm/execute.ts +74 -73
  27. package/src/orm/interceptor-pipeline.ts +21 -17
  28. package/src/orm/pooled-executor-factory.ts +41 -20
  29. package/src/orm/unit-of-work.ts +6 -4
  30. package/src/query/index.ts +8 -5
  31. package/src/query-builder/delete.ts +3 -2
  32. package/src/query-builder/insert-query-state.ts +47 -19
  33. package/src/query-builder/insert.ts +142 -28
  34. package/src/query-builder/procedure-call.ts +122 -0
  35. package/src/query-builder/select/select-operations.ts +5 -2
  36. package/src/query-builder/select.ts +1146 -1105
  37. package/src/query-builder/update.ts +3 -2
  38. package/src/tree/tree-manager.ts +754 -754
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -277,6 +277,8 @@ const renderEntityClassLines = ({ table, className, naming, relations, resolveCl
277
277
  lines.push(`@Entity(${entityOpts})`);
278
278
  lines.push(`export class ${className} {`);
279
279
 
280
+ const columnPropertyNames = new Set(table.columns.map(col => sanitizePropertyName(col.name)));
281
+
280
282
  for (const col of table.columns) {
281
283
  const propertyName = sanitizePropertyName(col.name);
282
284
  const rendered = renderColumnExpression(col, table.primaryKey, table.schema, defaultSchema, propertyName);
@@ -298,10 +300,31 @@ const renderEntityClassLines = ({ table, className, naming, relations, resolveCl
298
300
  const isSelfRefHasMany = (rel) =>
299
301
  treeConfig && rel.kind === 'hasMany' && rel.foreignKey === treeParentFK && isSelfRef(rel.target);
300
302
 
303
+ // Track used relation property names to avoid duplicates among relations themselves
304
+ const usedRelationProps = new Set();
305
+
301
306
  for (const rel of relations) {
302
307
  const targetClass = resolveClassName(rel.target);
303
308
  if (!targetClass) continue;
304
- const propName = naming.applyRelationOverride(className, rel.property);
309
+ let propName = naming.applyRelationOverride(className, rel.property);
310
+
311
+ // If the generated property name conflicts with a column property, fall back to
312
+ // using the camelCased target class name. This happens when a FK column has no
313
+ // "_id" suffix (e.g. "instancia", "criador") so the naming strategy derives the
314
+ // same name for both the column and the relation.
315
+ if (columnPropertyNames.has(propName)) {
316
+ const fallback = naming.toCamelCase(
317
+ naming.singularize(naming.normalizeTableName(rel.target))
318
+ );
319
+ propName = fallback;
320
+ }
321
+
322
+ // Resolve any remaining duplicate among relation properties by appending the FK name
323
+ if (usedRelationProps.has(propName)) {
324
+ propName = `${propName}_${naming.toCamelCase(rel.foreignKey)}`;
325
+ }
326
+ usedRelationProps.add(propName);
327
+
305
328
  switch (rel.kind) {
306
329
  case 'belongsTo':
307
330
  // For tree tables, replace @BelongsTo for parent with @TreeParent
@@ -95,10 +95,25 @@ export class BaseNamingStrategy {
95
95
 
96
96
  belongsToProperty(foreignKeyName, targetTable) {
97
97
  const trimmed = foreignKeyName.replace(/_?id$/i, '');
98
- const base = trimmed && trimmed !== foreignKeyName ? trimmed : this.singularize(targetTable);
98
+ const targetBase = this.singularize(this.normalizeTableName(targetTable));
99
+ // If FK name ends with _id, use the trimmed version
100
+ // If FK name doesn't end with _id but is different from target table name, use the FK name (e.g., "criador", "responsavel_judicial")
101
+ // Otherwise fallback to target table name
102
+ const base = trimmed && trimmed !== foreignKeyName
103
+ ? trimmed
104
+ : trimmed && this.toCamelCase(trimmed) !== this.toCamelCase(targetBase)
105
+ ? trimmed
106
+ : targetBase;
99
107
  return this.toCamelCase(base);
100
108
  }
101
109
 
110
+ normalizeTableName(tableName) {
111
+ // Strip schema prefix if present (e.g., "dbo.usuario" -> "usuario")
112
+ return typeof tableName === 'string' && tableName.includes('.')
113
+ ? tableName.split('.').pop()
114
+ : tableName;
115
+ }
116
+
102
117
  hasManyProperty(targetTable) {
103
118
  const base = this.singularize(targetTable);
104
119
  const plural = this.inflector.pluralizeRelationProperty
@@ -0,0 +1,21 @@
1
+ import type { OperandNode } from './expression.js';
2
+
3
+ export type ProcedureDirection = 'in' | 'out' | 'inout';
4
+
5
+ export interface ProcedureRefNode {
6
+ name: string;
7
+ schema?: string;
8
+ }
9
+
10
+ export interface ProcedureParamNode {
11
+ name: string;
12
+ direction: ProcedureDirection;
13
+ value?: OperandNode;
14
+ dbType?: string;
15
+ }
16
+
17
+ export interface ProcedureCallNode {
18
+ type: 'ProcedureCall';
19
+ ref: ProcedureRefNode;
20
+ params: ProcedureParamNode[];
21
+ }
@@ -161,25 +161,53 @@ export interface InsertValuesSourceNode {
161
161
  rows: OperandNode[][];
162
162
  }
163
163
 
164
- export interface InsertSelectSourceNode {
165
- type: 'InsertSelect';
166
- /** SELECT query providing rows */
167
- query: SelectQueryNode;
168
- }
169
-
170
- export type InsertSourceNode = InsertValuesSourceNode | InsertSelectSourceNode;
171
-
172
- export interface InsertQueryNode {
173
- type: 'InsertQuery';
174
- /** Target table */
175
- into: TableNode;
176
- /** Column order for inserted values */
177
- columns: ColumnNode[];
178
- /** Source of inserted rows (either literal values or a SELECT query) */
179
- source: InsertSourceNode;
180
- /** Optional RETURNING clause */
181
- returning?: ColumnNode[];
182
- }
164
+ export interface InsertSelectSourceNode {
165
+ type: 'InsertSelect';
166
+ /** SELECT query providing rows */
167
+ query: SelectQueryNode;
168
+ }
169
+
170
+ export type InsertSourceNode = InsertValuesSourceNode | InsertSelectSourceNode;
171
+
172
+ export interface UpsertConflictTarget {
173
+ /** Conflict columns (primary key or unique columns) */
174
+ columns: ColumnNode[];
175
+ /** Named constraint (PostgreSQL only) */
176
+ constraint?: string;
177
+ }
178
+
179
+ export interface UpsertUpdateAction {
180
+ type: 'DoUpdate';
181
+ /** Assignments to apply on conflict */
182
+ set: UpdateAssignmentNode[];
183
+ /** Optional condition for the update branch */
184
+ where?: ExpressionNode;
185
+ }
186
+
187
+ export interface UpsertDoNothingAction {
188
+ type: 'DoNothing';
189
+ }
190
+
191
+ export type UpsertAction = UpsertUpdateAction | UpsertDoNothingAction;
192
+
193
+ export interface UpsertClause {
194
+ target: UpsertConflictTarget;
195
+ action: UpsertAction;
196
+ }
197
+
198
+ export interface InsertQueryNode {
199
+ type: 'InsertQuery';
200
+ /** Target table */
201
+ into: TableNode;
202
+ /** Column order for inserted values */
203
+ columns: ColumnNode[];
204
+ /** Source of inserted rows (either literal values or a SELECT query) */
205
+ source: InsertSourceNode;
206
+ /** Optional dialect-specific UPSERT clause */
207
+ onConflict?: UpsertClause;
208
+ /** Optional RETURNING clause */
209
+ returning?: ColumnNode[];
210
+ }
183
211
 
184
212
  export interface UpdateAssignmentNode {
185
213
  /** Column to update */
@@ -1,56 +1,56 @@
1
- import { DbExecutor, QueryResult } from '../../execution/db-executor.js';
2
- import { IntrospectOptions } from './types.js';
3
-
4
- /**
5
- * Converts a query result to an array of row objects.
6
- * @param result - The query result.
7
- * @returns The array of rows.
8
- */
9
- export const toRows = (result: QueryResult | undefined): Record<string, unknown>[] => {
10
- if (!result) return [];
11
- return result.values.map(row =>
12
- result.columns.reduce<Record<string, unknown>>((acc, col, idx) => {
13
- acc[col] = row[idx];
14
- return acc;
15
- }, {})
16
- );
17
- };
18
-
19
- /**
20
- * Executes a SQL query and returns the rows.
21
- * @param executor - The database executor.
22
- * @param sql - The SQL query.
23
- * @param params - The query parameters.
24
- * @returns The array of rows.
25
- */
26
- export const queryRows = async (
27
- executor: DbExecutor,
28
- sql: string,
29
- params: unknown[] = []
30
- ): Promise<Record<string, unknown>[]> => {
31
- const [first] = await executor.executeSql(sql, params);
32
- return toRows(first);
33
- };
34
-
35
- /**
36
- * Checks if a table should be included in introspection based on options.
37
- * @param name - The table name.
38
- * @param options - The introspection options.
39
- * @returns True if the table should be included.
40
- */
41
- export const shouldIncludeTable = (name: string, options: IntrospectOptions): boolean => {
42
- if (options.includeTables && !options.includeTables.includes(name)) return false;
43
- if (options.excludeTables && options.excludeTables.includes(name)) return false;
44
- return true;
45
- };
46
-
47
- /**
48
- * Checks if a view should be included in introspection based on options.
49
- * @param name - The view name.
50
- * @param options - The introspection options.
51
- * @returns True if the view should be included.
52
- */
53
- export const shouldIncludeView = (name: string, options: IntrospectOptions): boolean => {
54
- if (options.excludeViews && options.excludeViews.includes(name)) return false;
55
- return true;
56
- };
1
+ import { DbExecutor, QueryResult } from '../../execution/db-executor.js';
2
+ import { IntrospectOptions } from './types.js';
3
+
4
+ /**
5
+ * Converts a query result to an array of row objects.
6
+ * @param result - The query result.
7
+ * @returns The array of rows.
8
+ */
9
+ export const toRows = (result: QueryResult | undefined): Record<string, unknown>[] => {
10
+ if (!result) return [];
11
+ return result.values.map(row =>
12
+ result.columns.reduce<Record<string, unknown>>((acc, col, idx) => {
13
+ acc[col] = row[idx];
14
+ return acc;
15
+ }, {})
16
+ );
17
+ };
18
+
19
+ /**
20
+ * Executes a SQL query and returns the rows.
21
+ * @param executor - The database executor.
22
+ * @param sql - The SQL query.
23
+ * @param params - The query parameters.
24
+ * @returns The array of rows.
25
+ */
26
+ export const queryRows = async (
27
+ executor: DbExecutor,
28
+ sql: string,
29
+ params: unknown[] = []
30
+ ): Promise<Record<string, unknown>[]> => {
31
+ const [first] = await executor.executeSql(sql, params);
32
+ return toRows(first);
33
+ };
34
+
35
+ /**
36
+ * Checks if a table should be included in introspection based on options.
37
+ * @param name - The table name.
38
+ * @param options - The introspection options.
39
+ * @returns True if the table should be included.
40
+ */
41
+ export const shouldIncludeTable = (name: string, options: IntrospectOptions): boolean => {
42
+ if (options.includeTables && !options.includeTables.includes(name)) return false;
43
+ if (options.excludeTables && options.excludeTables.includes(name)) return false;
44
+ return true;
45
+ };
46
+
47
+ /**
48
+ * Checks if a view should be included in introspection based on options.
49
+ * @param name - The view name.
50
+ * @param options - The introspection options.
51
+ * @returns True if the view should be included.
52
+ */
53
+ export const shouldIncludeView = (name: string, options: IntrospectOptions): boolean => {
54
+ if (options.excludeViews && options.excludeViews.includes(name)) return false;
55
+ return true;
56
+ };