metal-orm 1.0.81 → 1.0.83

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.81",
3
+ "version": "1.0.83",
4
4
  "type": "module",
5
5
  "types": "./dist/index.d.ts",
6
6
  "engines": {
@@ -17,13 +17,14 @@ import {
17
17
  LiteralNode,
18
18
  FunctionNode,
19
19
  AliasRefNode,
20
- CastExpressionNode,
21
- CollateExpressionNode,
22
- ExpressionVisitor,
23
- OperandVisitor,
24
- visitExpression,
25
- visitOperand
26
- } from '../core/ast/expression.js';
20
+ CastExpressionNode,
21
+ CollateExpressionNode,
22
+ ParamNode,
23
+ ExpressionVisitor,
24
+ OperandVisitor,
25
+ visitExpression,
26
+ visitOperand
27
+ } from '../core/ast/expression.js';
27
28
  import { SQL_OPERATOR_REGISTRY } from '../core/sql/sql-operator-config.js';
28
29
  import { SqlOperator } from '../core/sql/sql.js';
29
30
  import { isRelationAlias } from '../query-builder/relation-alias.js';
@@ -190,13 +191,14 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
190
191
  case 'ScalarSubquery':
191
192
  case 'CaseExpression':
192
193
  case 'WindowFunction':
193
- case 'Cast':
194
- case 'Collate':
195
- return this.printOperand(term);
196
- default:
197
- return this.printExpression(term);
198
- }
199
- }
194
+ case 'Cast':
195
+ case 'Collate':
196
+ case 'Param':
197
+ return this.printOperand(term);
198
+ default:
199
+ return this.printExpression(term);
200
+ }
201
+ }
200
202
 
201
203
  private getSelectionKey(selection: SelectionColumn, index: number): string {
202
204
  if (selection.alias) {
@@ -244,13 +246,17 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
244
246
  return this.printColumnOperand(node);
245
247
  }
246
248
 
247
- public visitLiteral(node: LiteralNode): string {
248
- return this.printLiteralOperand(node);
249
- }
250
-
251
- public visitFunction(node: FunctionNode): string {
252
- return this.printFunctionOperand(node);
253
- }
249
+ public visitLiteral(node: LiteralNode): string {
250
+ return this.printLiteralOperand(node);
251
+ }
252
+
253
+ public visitParam(node: ParamNode): string {
254
+ return this.printParamOperand(node);
255
+ }
256
+
257
+ public visitFunction(node: FunctionNode): string {
258
+ return this.printFunctionOperand(node);
259
+ }
254
260
 
255
261
  public visitJsonPath(node: JsonPathNode): string {
256
262
  return this.printJsonPathOperand(node);
@@ -379,14 +385,19 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
379
385
  * @param literal - Literal node
380
386
  * @returns TypeScript code representation
381
387
  */
382
- private printLiteralOperand(literal: LiteralNode): string {
383
- if (literal.value === null) return 'null';
384
- return typeof literal.value === 'string' ? `'${literal.value}'` : String(literal.value);
385
- }
386
-
387
- /**
388
- * Prints a function operand to TypeScript code
389
- * @param fn - Function node
388
+ private printLiteralOperand(literal: LiteralNode): string {
389
+ if (literal.value === null) return 'null';
390
+ return typeof literal.value === 'string' ? `'${literal.value}'` : String(literal.value);
391
+ }
392
+
393
+ private printParamOperand(param: ParamNode): string {
394
+ const name = param.name.replace(/'/g, "\\'");
395
+ return `{ type: 'Param', name: '${name}' }`;
396
+ }
397
+
398
+ /**
399
+ * Prints a function operand to TypeScript code
400
+ * @param fn - Function node
390
401
  * @returns TypeScript code representation
391
402
  */
392
403
  private printFunctionOperand(fn: FunctionNode): string {
@@ -0,0 +1,188 @@
1
+ import type { ExpressionNode, OperandNode } from './expression-nodes.js';
2
+ import { visitExpression, visitOperand } from './expression-visitor.js';
3
+ import type { SelectQueryNode } from './query.js';
4
+
5
+ const hasParamOperandsInExpression = (expr: ExpressionNode): boolean => {
6
+ let hasParams = false;
7
+
8
+ visitExpression(expr, {
9
+ visitBinaryExpression: (node) => {
10
+ visitOperand(node.left, {
11
+ visitParam: () => { hasParams = true; },
12
+ otherwise: () => {}
13
+ });
14
+ visitOperand(node.right, {
15
+ visitParam: () => { hasParams = true; },
16
+ otherwise: () => {}
17
+ });
18
+ if (node.escape) {
19
+ visitOperand(node.escape, {
20
+ visitParam: () => { hasParams = true; },
21
+ otherwise: () => {}
22
+ });
23
+ }
24
+ },
25
+ visitLogicalExpression: (node) => {
26
+ node.operands.forEach(operand => {
27
+ if (hasParamOperandsInExpression(operand)) {
28
+ hasParams = true;
29
+ }
30
+ });
31
+ },
32
+ visitNullExpression: () => {},
33
+ visitInExpression: (node) => {
34
+ visitOperand(node.left, {
35
+ visitParam: () => { hasParams = true; },
36
+ otherwise: () => {}
37
+ });
38
+ if (Array.isArray(node.right)) {
39
+ node.right.forEach(operand => visitOperand(operand, {
40
+ visitParam: () => { hasParams = true; },
41
+ otherwise: () => {}
42
+ }));
43
+ }
44
+ },
45
+ visitExistsExpression: () => {},
46
+ visitBetweenExpression: (node) => {
47
+ visitOperand(node.left, {
48
+ visitParam: () => { hasParams = true; },
49
+ otherwise: () => {}
50
+ });
51
+ visitOperand(node.lower, {
52
+ visitParam: () => { hasParams = true; },
53
+ otherwise: () => {}
54
+ });
55
+ visitOperand(node.upper, {
56
+ visitParam: () => { hasParams = true; },
57
+ otherwise: () => {}
58
+ });
59
+ },
60
+ visitArithmeticExpression: (node) => {
61
+ visitOperand(node.left, {
62
+ visitParam: () => { hasParams = true; },
63
+ otherwise: () => {}
64
+ });
65
+ visitOperand(node.right, {
66
+ visitParam: () => { hasParams = true; },
67
+ otherwise: () => {}
68
+ });
69
+ },
70
+ visitBitwiseExpression: (node) => {
71
+ visitOperand(node.left, {
72
+ visitParam: () => { hasParams = true; },
73
+ otherwise: () => {}
74
+ });
75
+ visitOperand(node.right, {
76
+ visitParam: () => { hasParams = true; },
77
+ otherwise: () => {}
78
+ });
79
+ },
80
+ otherwise: () => {}
81
+ });
82
+
83
+ return hasParams;
84
+ };
85
+
86
+ const hasParamOperandsInOperand = (operand: OperandNode): boolean => {
87
+ let hasParams = false;
88
+
89
+ visitOperand(operand, {
90
+ visitColumn: () => {},
91
+ visitLiteral: () => {},
92
+ visitParam: () => { hasParams = true; },
93
+ visitFunction: (node) => {
94
+ node.args?.forEach(arg => {
95
+ if (hasParamOperandsInOperand(arg)) {
96
+ hasParams = true;
97
+ }
98
+ });
99
+ },
100
+ visitJsonPath: () => {},
101
+ visitScalarSubquery: () => {},
102
+ visitCaseExpression: (node) => {
103
+ node.conditions.forEach(cond => {
104
+ if (hasParamOperandsInExpression(cond.when)) {
105
+ hasParams = true;
106
+ }
107
+ if (hasParamOperandsInOperand(cond.then)) {
108
+ hasParams = true;
109
+ }
110
+ });
111
+ if (node.else && hasParamOperandsInOperand(node.else)) {
112
+ hasParams = true;
113
+ }
114
+ },
115
+ visitCast: (node) => {
116
+ if (hasParamOperandsInOperand(node.expression)) {
117
+ hasParams = true;
118
+ }
119
+ },
120
+ visitWindowFunction: (node) => {
121
+ node.args?.forEach(arg => {
122
+ if (hasParamOperandsInOperand(arg)) {
123
+ hasParams = true;
124
+ }
125
+ });
126
+ node.orderBy?.forEach(ord => {
127
+ if (ord.term) {
128
+ if (hasParamOperandsInOperand(ord.term as OperandNode)) {
129
+ hasParams = true;
130
+ }
131
+ }
132
+ });
133
+ },
134
+ visitCollate: (node) => {
135
+ if (hasParamOperandsInOperand(node.expression)) {
136
+ hasParams = true;
137
+ }
138
+ },
139
+ visitAliasRef: () => {},
140
+ otherwise: () => {}
141
+ });
142
+
143
+ return hasParams;
144
+ };
145
+
146
+ export const hasParamOperandsInQuery = (ast: SelectQueryNode): boolean => {
147
+ if (ast.where && hasParamOperandsInExpression(ast.where)) {
148
+ return true;
149
+ }
150
+
151
+ if (ast.having && hasParamOperandsInExpression(ast.having)) {
152
+ return true;
153
+ }
154
+
155
+ ast.columns?.forEach(col => {
156
+ if (typeof col === 'object' && col !== null && 'type' in col) {
157
+ if (hasParamOperandsInOperand(col as OperandNode)) {
158
+ return true;
159
+ }
160
+ }
161
+ });
162
+
163
+ ast.orderBy?.forEach(ord => {
164
+ if (ord.term) {
165
+ if (hasParamOperandsInOperand(ord.term as OperandNode)) {
166
+ return true;
167
+ }
168
+ }
169
+ });
170
+
171
+ if (ast.ctes) {
172
+ for (const cte of ast.ctes) {
173
+ if (cte.query.where && hasParamOperandsInExpression(cte.query.where)) {
174
+ return true;
175
+ }
176
+ }
177
+ }
178
+
179
+ if (ast.setOps) {
180
+ for (const op of ast.setOps) {
181
+ if (hasParamOperandsInQuery(op.query)) {
182
+ return true;
183
+ }
184
+ }
185
+ }
186
+
187
+ return false;
188
+ };
@@ -2,13 +2,14 @@ import { SelectQueryNode } from './query.js';
2
2
  import { SqlOperator, BitwiseOperator } from '../sql/sql.js';
3
3
  import { ColumnRef } from './types.js';
4
4
  import {
5
- ColumnNode,
6
- LiteralNode,
7
- JsonPathNode,
8
- OperandNode,
9
- CaseExpressionNode,
10
- CastExpressionNode,
11
- BinaryExpressionNode,
5
+ ColumnNode,
6
+ LiteralNode,
7
+ ParamNode,
8
+ JsonPathNode,
9
+ OperandNode,
10
+ CaseExpressionNode,
11
+ CastExpressionNode,
12
+ BinaryExpressionNode,
12
13
  ExpressionNode,
13
14
  LogicalExpressionNode,
14
15
  NullExpressionNode,
@@ -47,12 +48,21 @@ const toLiteralNode = (value: LiteralValue): LiteralNode => ({
47
48
  type: 'Literal',
48
49
  value: value instanceof Date ? value.toISOString() : value
49
50
  });
50
-
51
- /**
52
- * Converts a ColumnRef to a ColumnNode
53
- * @throws Error if the ColumnRef doesn't have a table specified
54
- */
55
- const columnRefToNode = (col: ColumnRef): ColumnNode => {
51
+
52
+ const toParamNode = (value: unknown): ParamNode | undefined => {
53
+ if (typeof value !== 'object' || value === null) return undefined;
54
+ const type = Object.getOwnPropertyDescriptor(value, 'type')?.value;
55
+ if (type !== 'Param') return undefined;
56
+ const name = Object.getOwnPropertyDescriptor(value, 'name')?.value;
57
+ if (typeof name !== 'string') return undefined;
58
+ return { type: 'Param', name };
59
+ };
60
+
61
+ /**
62
+ * Converts a ColumnRef to a ColumnNode
63
+ * @throws Error if the ColumnRef doesn't have a table specified
64
+ */
65
+ const columnRefToNode = (col: ColumnRef): ColumnNode => {
56
66
  if (!col.table) {
57
67
  throw new Error(
58
68
  `Column "${col.name}" requires a table reference. ` +
@@ -67,11 +77,16 @@ const columnRefToNode = (col: ColumnRef): ColumnNode => {
67
77
  * @param value - Value to convert (OperandNode, ColumnRef, or literal value)
68
78
  * @returns OperandNode representing the value
69
79
  */
70
- const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNode => {
71
- // Already an operand node
72
- if (isOperandNode(value)) {
73
- return value;
74
- }
80
+ const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNode => {
81
+ const paramNode = toParamNode(value);
82
+ if (paramNode) {
83
+ return paramNode;
84
+ }
85
+
86
+ // Already an operand node
87
+ if (isOperandNode(value)) {
88
+ return value;
89
+ }
75
90
 
76
91
  // Literal value
77
92
  if (isLiteralValue(value)) {
@@ -87,12 +102,16 @@ const toOperandNode = (value: OperandNode | ColumnRef | LiteralValue): OperandNo
87
102
  * @param value - Value or operand to normalize
88
103
  * @returns OperandNode representing the value
89
104
  */
90
- export const valueToOperand = (value: ValueOperandInput): OperandNode => {
91
- if (isOperandNode(value)) {
92
- return value;
93
- }
94
- return toLiteralNode(value);
95
- };
105
+ export const valueToOperand = (value: ValueOperandInput): OperandNode => {
106
+ const paramNode = toParamNode(value);
107
+ if (paramNode) {
108
+ return paramNode;
109
+ }
110
+ if (isOperandNode(value)) {
111
+ return value;
112
+ }
113
+ return toLiteralNode(value);
114
+ };
96
115
 
97
116
  /**
98
117
  * Converts various input types to an OperandNode
@@ -2,14 +2,23 @@ import type { SelectQueryNode, OrderByNode } from './query.js';
2
2
  import { SqlOperator, BitwiseOperator } from '../sql/sql.js';
3
3
  import { ColumnRef } from './types.js';
4
4
 
5
- /**
6
- * AST node representing a literal value
7
- */
5
+ /**
6
+ * AST node representing a literal value
7
+ */
8
8
  export interface LiteralNode {
9
9
  type: 'Literal';
10
10
  /** The literal value (string, number, boolean, Date, or null) */
11
11
  value: string | number | boolean | Date | null;
12
12
  }
13
+
14
+ /**
15
+ * AST node representing a named parameter placeholder
16
+ */
17
+ export interface ParamNode {
18
+ type: 'Param';
19
+ /** Stable parameter name */
20
+ name: string;
21
+ }
13
22
 
14
23
  /**
15
24
  * AST node representing a reference to a SELECT alias (for ORDER BY / GROUP BY).
@@ -149,29 +158,31 @@ export interface ArithmeticExpressionNode {
149
158
  /**
150
159
  * Union type representing any operand that can be used in expressions
151
160
  */
152
- export type OperandNode =
153
- | AliasRefNode
154
- | ColumnNode
155
- | LiteralNode
156
- | FunctionNode
157
- | JsonPathNode
158
- | ScalarSubqueryNode
159
- | CaseExpressionNode
160
- | CastExpressionNode
161
+ export type OperandNode =
162
+ | AliasRefNode
163
+ | ColumnNode
164
+ | LiteralNode
165
+ | ParamNode
166
+ | FunctionNode
167
+ | JsonPathNode
168
+ | ScalarSubqueryNode
169
+ | CaseExpressionNode
170
+ | CastExpressionNode
161
171
  | WindowFunctionNode
162
172
  | ArithmeticExpressionNode
163
173
  | BitwiseExpressionNode
164
174
  | CollateExpressionNode;
165
175
 
166
- const operandTypes = new Set<OperandNode['type']>([
167
- 'AliasRef',
168
- 'Column',
169
- 'Literal',
170
- 'Function',
171
- 'JsonPath',
172
- 'ScalarSubquery',
173
- 'CaseExpression',
174
- 'Cast',
176
+ const operandTypes = new Set<OperandNode['type']>([
177
+ 'AliasRef',
178
+ 'Column',
179
+ 'Literal',
180
+ 'Param',
181
+ 'Function',
182
+ 'JsonPath',
183
+ 'ScalarSubquery',
184
+ 'CaseExpression',
185
+ 'Cast',
175
186
  'WindowFunction',
176
187
  'ArithmeticExpression',
177
188
  'BitwiseExpression',
@@ -18,7 +18,8 @@ import {
18
18
  WindowFunctionNode,
19
19
  CollateExpressionNode,
20
20
  AliasRefNode,
21
- BitwiseExpressionNode
21
+ BitwiseExpressionNode,
22
+ ParamNode
22
23
  } from './expression-nodes.js';
23
24
 
24
25
  /**
@@ -42,6 +43,7 @@ export interface ExpressionVisitor<R> {
42
43
  export interface OperandVisitor<R> {
43
44
  visitColumn?(node: ColumnNode): R;
44
45
  visitLiteral?(node: LiteralNode): R;
46
+ visitParam?(node: ParamNode): R;
45
47
  visitFunction?(node: FunctionNode): R;
46
48
  visitJsonPath?(node: JsonPathNode): R;
47
49
  visitScalarSubquery?(node: ScalarSubqueryNode): R;
@@ -187,6 +189,9 @@ export const visitOperand = <R>(node: OperandNode, visitor: OperandVisitor<R>):
187
189
  case 'Literal':
188
190
  if (visitor.visitLiteral) return visitor.visitLiteral(node);
189
191
  break;
192
+ case 'Param':
193
+ if (visitor.visitParam) return visitor.visitParam(node);
194
+ break;
190
195
  case 'Function':
191
196
  if (visitor.visitFunction) return visitor.visitFunction(node);
192
197
  break;
@@ -9,6 +9,7 @@ export * from './expression-builders.js';
9
9
  export * from './window-functions.js';
10
10
  export * from './aggregate-functions.js';
11
11
  export * from './expression-visitor.js';
12
+ export * from './param-proxy.js';
12
13
  export type { ColumnRef, TableRef as AstTableRef } from './types.js';
13
14
  export * from './adapters.js';
14
15
 
@@ -0,0 +1,47 @@
1
+ import type { ParamNode } from './expression-nodes.js';
2
+
3
+ export type ParamProxy = ParamNode & {
4
+ [key: string]: ParamProxy;
5
+ };
6
+
7
+ export type ParamProxyRoot = {
8
+ [key: string]: ParamProxy;
9
+ };
10
+
11
+ const buildParamProxy = (name: string): ParamProxy => {
12
+ const target: ParamNode = { type: 'Param', name };
13
+ return new Proxy(target, {
14
+ get(t, prop, receiver) {
15
+ if (prop === 'then') return undefined;
16
+ if (typeof prop === 'symbol') {
17
+ return Reflect.get(t, prop, receiver);
18
+ }
19
+ if (typeof prop === 'string' && prop.startsWith('$')) {
20
+ const trimmed = prop.slice(1);
21
+ const nextName = name ? `${name}.${trimmed}` : trimmed;
22
+ return buildParamProxy(nextName);
23
+ }
24
+ if (prop in t && name === '') {
25
+ return (t as unknown as Record<string, unknown>)[prop];
26
+ }
27
+ const nextName = name ? `${name}.${prop}` : prop;
28
+ return buildParamProxy(nextName);
29
+ }
30
+ }) as ParamProxy;
31
+ };
32
+
33
+ export const createParamProxy = (): ParamProxyRoot => {
34
+ const target: Record<string, unknown> = {};
35
+ return new Proxy(target, {
36
+ get(t, prop, receiver) {
37
+ if (prop === 'then') return undefined;
38
+ if (typeof prop === 'symbol') {
39
+ return Reflect.get(t, prop, receiver);
40
+ }
41
+ if (typeof prop === 'string' && prop.startsWith('$')) {
42
+ return buildParamProxy(prop.slice(1));
43
+ }
44
+ return buildParamProxy(String(prop));
45
+ }
46
+ }) as ParamProxyRoot;
47
+ };