metal-orm 1.0.79 → 1.0.81
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/dist/index.cjs +795 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +174 -2
- package/dist/index.d.ts +174 -2
- package/dist/index.js +788 -5
- package/dist/index.js.map +1 -1
- package/package.json +10 -2
- package/src/core/dialect/postgres/index.ts +9 -5
- package/src/core/execution/db-executor.ts +5 -4
- package/src/core/execution/executors/mysql-executor.ts +9 -7
- package/src/index.ts +6 -4
- package/src/openapi/index.ts +4 -0
- package/src/openapi/query-parameters.ts +206 -0
- package/src/openapi/schema-extractor.ts +586 -0
- package/src/openapi/schema-types.ts +158 -0
- package/src/openapi/type-mappers.ts +227 -0
- package/src/orm/unit-of-work.ts +25 -13
- package/src/query-builder/select.ts +35 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metal-orm",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.81",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"engines": {
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"gen:entities": "node scripts/generate-entities.mjs",
|
|
30
30
|
"test": "vitest",
|
|
31
31
|
"test:ui": "vitest --ui",
|
|
32
|
+
"test:mysql": "vitest --run tests/e2e/mysql-memory.test.ts tests/e2e/decorators-mysql-memory.test.ts tests/e2e/save-graph-mysql-memory.test.ts",
|
|
33
|
+
"test:sqlite": "vitest --run tests/e2e/sqlite-memory.test.ts tests/e2e/decorators-sqlite-memory.test.ts tests/e2e/save-graph-sqlite-memory.test.ts",
|
|
34
|
+
"test:pglite": "vitest --run tests/e2e/pglite-memory.test.ts",
|
|
35
|
+
"test:e2e": "vitest tests/e2e",
|
|
32
36
|
"show-sql": "node scripts/show-sql.mjs",
|
|
33
37
|
"lint": "node scripts/run-eslint.mjs",
|
|
34
38
|
"lint:fix": "node scripts/run-eslint.mjs --fix"
|
|
@@ -54,11 +58,15 @@
|
|
|
54
58
|
}
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
|
-
"@
|
|
61
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
62
|
+
"@types/express": "^5.0.6",
|
|
58
63
|
"@typescript-eslint/eslint-plugin": "^8.20.0",
|
|
59
64
|
"@typescript-eslint/parser": "^8.20.0",
|
|
65
|
+
"@vitest/ui": "^4.0.14",
|
|
60
66
|
"eslint": "^8.57.0",
|
|
61
67
|
"eslint-plugin-deprecation": "^3.0.0",
|
|
68
|
+
"express": "^5.2.1",
|
|
69
|
+
"mysql-memory-server": "^1.13.0",
|
|
62
70
|
"mysql2": "^3.15.3",
|
|
63
71
|
"pg": "^8.16.3",
|
|
64
72
|
"sqlite3": "^5.1.7",
|
|
@@ -33,11 +33,15 @@ export class PostgresDialect extends SqlDialectBase {
|
|
|
33
33
|
* @param id - Identifier to quote
|
|
34
34
|
* @returns Quoted identifier
|
|
35
35
|
*/
|
|
36
|
-
quoteIdentifier(id: string): string {
|
|
37
|
-
return `"${id}"`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
quoteIdentifier(id: string): string {
|
|
37
|
+
return `"${id}"`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected formatPlaceholder(index: number): string {
|
|
41
|
+
return `$${index}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
41
45
|
* Compiles JSON path expression using PostgreSQL syntax
|
|
42
46
|
* @param node - JSON path node
|
|
43
47
|
* @returns PostgreSQL JSON path expression
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// src/core/execution/db-executor.ts
|
|
2
2
|
|
|
3
3
|
// low-level canonical shape
|
|
4
|
-
export type QueryResult = {
|
|
5
|
-
columns: string[];
|
|
6
|
-
values: unknown[][];
|
|
7
|
-
|
|
4
|
+
export type QueryResult = {
|
|
5
|
+
columns: string[];
|
|
6
|
+
values: unknown[][];
|
|
7
|
+
insertId?: number;
|
|
8
|
+
};
|
|
8
9
|
|
|
9
10
|
export interface DbExecutor {
|
|
10
11
|
/** Capability flags so the runtime can make correct decisions without relying on optional methods. */
|
|
@@ -31,13 +31,15 @@ export function createMysqlExecutor(
|
|
|
31
31
|
capabilities: {
|
|
32
32
|
transactions: supportsTransactions,
|
|
33
33
|
},
|
|
34
|
-
async executeSql(sql, params) {
|
|
35
|
-
const [rows] = await client.query(sql, params);
|
|
36
|
-
|
|
37
|
-
if (!Array.isArray(rows)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
async executeSql(sql, params) {
|
|
35
|
+
const [rows] = await client.query(sql, params);
|
|
36
|
+
|
|
37
|
+
if (!Array.isArray(rows)) {
|
|
38
|
+
const insertId = (rows as { insertId?: number } | null)?.insertId;
|
|
39
|
+
const normalized = typeof insertId === 'number' && insertId > 0 ? insertId : undefined;
|
|
40
|
+
// e.g. insert/update returning only headers, treat as no rows
|
|
41
|
+
return [{ columns: [], values: [], insertId: normalized }];
|
|
42
|
+
}
|
|
41
43
|
|
|
42
44
|
const result = rowsToQueryResult(
|
|
43
45
|
rows as Array<Record<string, unknown>>
|
package/src/index.ts
CHANGED
|
@@ -41,16 +41,18 @@ export * from './orm/relations/has-many.js';
|
|
|
41
41
|
export * from './orm/relations/belongs-to.js';
|
|
42
42
|
export * from './orm/relations/many-to-many.js';
|
|
43
43
|
export * from './orm/execute.js';
|
|
44
|
-
export type { EntityContext } from './orm/entity-context.js';
|
|
45
|
-
export type { PrimaryKey as EntityPrimaryKey } from './orm/entity-context.js';
|
|
44
|
+
export type { EntityContext } from './orm/entity-context.js';
|
|
45
|
+
export type { PrimaryKey as EntityPrimaryKey } from './orm/entity-context.js';
|
|
46
46
|
export * from './orm/execution-context.js';
|
|
47
47
|
export * from './orm/hydration-context.js';
|
|
48
48
|
export * from './orm/domain-event-bus.js';
|
|
49
49
|
export * from './orm/runtime-types.js';
|
|
50
50
|
export * from './orm/query-logger.js';
|
|
51
|
+
export * from './orm/interceptor-pipeline.js';
|
|
51
52
|
export * from './orm/jsonify.js';
|
|
52
|
-
export * from './orm/save-graph-types.js';
|
|
53
|
-
export * from './decorators/index.js';
|
|
53
|
+
export * from './orm/save-graph-types.js';
|
|
54
|
+
export * from './decorators/index.js';
|
|
55
|
+
export * from './openapi/index.js';
|
|
54
56
|
|
|
55
57
|
// NEW: execution abstraction + helpers
|
|
56
58
|
export * from './core/execution/db-executor.js';
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { TableDef } from '../schema/table.js';
|
|
2
|
+
import {
|
|
3
|
+
isOperandNode,
|
|
4
|
+
type ExpressionNode,
|
|
5
|
+
type OperandNode,
|
|
6
|
+
type ColumnNode,
|
|
7
|
+
type FunctionNode,
|
|
8
|
+
type JsonPathNode,
|
|
9
|
+
type CaseExpressionNode,
|
|
10
|
+
type CastExpressionNode,
|
|
11
|
+
type WindowFunctionNode,
|
|
12
|
+
type ArithmeticExpressionNode,
|
|
13
|
+
type BitwiseExpressionNode,
|
|
14
|
+
type CollateExpressionNode
|
|
15
|
+
} from '../core/ast/expression.js';
|
|
16
|
+
import type { TableSourceNode, OrderByNode, OrderingTerm } from '../core/ast/query.js';
|
|
17
|
+
import type { ColumnSchemaOptions, JsonSchemaProperty, OpenApiParameter } from './schema-types.js';
|
|
18
|
+
import { mapColumnType } from './type-mappers.js';
|
|
19
|
+
|
|
20
|
+
const FILTER_PARAM_NAME = 'filter';
|
|
21
|
+
|
|
22
|
+
const buildRootTableNames = (table: TableDef, from?: TableSourceNode): Set<string> => {
|
|
23
|
+
const names = new Set<string>([table.name]);
|
|
24
|
+
if (from?.type === 'Table') {
|
|
25
|
+
names.add(from.name);
|
|
26
|
+
if (from.alias) names.add(from.alias);
|
|
27
|
+
}
|
|
28
|
+
return names;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const collectFilterColumns = (
|
|
32
|
+
expr: ExpressionNode,
|
|
33
|
+
table: TableDef,
|
|
34
|
+
rootTables: Set<string>
|
|
35
|
+
): Set<string> => {
|
|
36
|
+
const columns = new Set<string>();
|
|
37
|
+
|
|
38
|
+
const recordColumn = (node: ColumnNode): void => {
|
|
39
|
+
if (!rootTables.has(node.table)) return;
|
|
40
|
+
if (node.name in table.columns) {
|
|
41
|
+
columns.add(node.name);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const visitOrderingTerm = (term: OrderingTerm): void => {
|
|
46
|
+
if (!term || typeof term !== 'object') return;
|
|
47
|
+
if (isOperandNode(term)) {
|
|
48
|
+
visitOperand(term as OperandNode);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if ('type' in term) {
|
|
52
|
+
visitExpression(term as ExpressionNode);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const visitOrderBy = (orderBy: OrderByNode[] | undefined): void => {
|
|
57
|
+
if (!orderBy) return;
|
|
58
|
+
orderBy.forEach(node => visitOrderingTerm(node.term));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const visitOperand = (node: OperandNode): void => {
|
|
62
|
+
switch (node.type) {
|
|
63
|
+
case 'Column':
|
|
64
|
+
recordColumn(node as ColumnNode);
|
|
65
|
+
return;
|
|
66
|
+
case 'Function': {
|
|
67
|
+
const fn = node as FunctionNode;
|
|
68
|
+
fn.args?.forEach(visitOperand);
|
|
69
|
+
visitOrderBy(fn.orderBy);
|
|
70
|
+
if (fn.separator) visitOperand(fn.separator);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
case 'JsonPath': {
|
|
74
|
+
const jp = node as JsonPathNode;
|
|
75
|
+
recordColumn(jp.column);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case 'ScalarSubquery':
|
|
79
|
+
return;
|
|
80
|
+
case 'CaseExpression': {
|
|
81
|
+
const cs = node as CaseExpressionNode;
|
|
82
|
+
cs.conditions.forEach(condition => {
|
|
83
|
+
visitExpression(condition.when);
|
|
84
|
+
visitOperand(condition.then);
|
|
85
|
+
});
|
|
86
|
+
if (cs.else) visitOperand(cs.else);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
case 'Cast': {
|
|
90
|
+
const cast = node as CastExpressionNode;
|
|
91
|
+
visitOperand(cast.expression);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
case 'WindowFunction': {
|
|
95
|
+
const windowFn = node as WindowFunctionNode;
|
|
96
|
+
windowFn.args?.forEach(visitOperand);
|
|
97
|
+
windowFn.partitionBy?.forEach(recordColumn);
|
|
98
|
+
visitOrderBy(windowFn.orderBy);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
case 'ArithmeticExpression': {
|
|
102
|
+
const arith = node as ArithmeticExpressionNode;
|
|
103
|
+
visitOperand(arith.left);
|
|
104
|
+
visitOperand(arith.right);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
case 'BitwiseExpression': {
|
|
108
|
+
const bitwise = node as BitwiseExpressionNode;
|
|
109
|
+
visitOperand(bitwise.left);
|
|
110
|
+
visitOperand(bitwise.right);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
case 'Collate': {
|
|
114
|
+
const collate = node as CollateExpressionNode;
|
|
115
|
+
visitOperand(collate.expression);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
case 'AliasRef':
|
|
119
|
+
case 'Literal':
|
|
120
|
+
return;
|
|
121
|
+
default:
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const visitExpression = (node: ExpressionNode): void => {
|
|
127
|
+
switch (node.type) {
|
|
128
|
+
case 'BinaryExpression':
|
|
129
|
+
visitOperand(node.left);
|
|
130
|
+
visitOperand(node.right);
|
|
131
|
+
if (node.escape) visitOperand(node.escape);
|
|
132
|
+
return;
|
|
133
|
+
case 'LogicalExpression':
|
|
134
|
+
node.operands.forEach(visitExpression);
|
|
135
|
+
return;
|
|
136
|
+
case 'NullExpression':
|
|
137
|
+
visitOperand(node.left);
|
|
138
|
+
return;
|
|
139
|
+
case 'InExpression':
|
|
140
|
+
visitOperand(node.left);
|
|
141
|
+
if (Array.isArray(node.right)) {
|
|
142
|
+
node.right.forEach(visitOperand);
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
case 'ExistsExpression':
|
|
146
|
+
return;
|
|
147
|
+
case 'BetweenExpression':
|
|
148
|
+
visitOperand(node.left);
|
|
149
|
+
visitOperand(node.lower);
|
|
150
|
+
visitOperand(node.upper);
|
|
151
|
+
return;
|
|
152
|
+
case 'ArithmeticExpression':
|
|
153
|
+
visitOperand(node.left);
|
|
154
|
+
visitOperand(node.right);
|
|
155
|
+
return;
|
|
156
|
+
case 'BitwiseExpression':
|
|
157
|
+
visitOperand(node.left);
|
|
158
|
+
visitOperand(node.right);
|
|
159
|
+
return;
|
|
160
|
+
default:
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
visitExpression(expr);
|
|
166
|
+
return columns;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export const buildFilterParameters = (
|
|
170
|
+
table: TableDef,
|
|
171
|
+
where: ExpressionNode | undefined,
|
|
172
|
+
from: TableSourceNode | undefined,
|
|
173
|
+
options: ColumnSchemaOptions = {}
|
|
174
|
+
): OpenApiParameter[] => {
|
|
175
|
+
if (!where) return [];
|
|
176
|
+
|
|
177
|
+
const rootTables = buildRootTableNames(table, from);
|
|
178
|
+
const columnNames = collectFilterColumns(where, table, rootTables);
|
|
179
|
+
|
|
180
|
+
let schema: JsonSchemaProperty;
|
|
181
|
+
if (columnNames.size) {
|
|
182
|
+
const properties: Record<string, JsonSchemaProperty> = {};
|
|
183
|
+
for (const name of columnNames) {
|
|
184
|
+
const column = table.columns[name];
|
|
185
|
+
if (!column) continue;
|
|
186
|
+
properties[name] = mapColumnType(column, options);
|
|
187
|
+
}
|
|
188
|
+
schema = {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties
|
|
191
|
+
};
|
|
192
|
+
} else {
|
|
193
|
+
schema = {
|
|
194
|
+
type: 'object',
|
|
195
|
+
additionalProperties: true
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return [{
|
|
200
|
+
name: FILTER_PARAM_NAME,
|
|
201
|
+
in: 'query',
|
|
202
|
+
style: 'deepObject',
|
|
203
|
+
explode: true,
|
|
204
|
+
schema
|
|
205
|
+
}];
|
|
206
|
+
};
|