metal-orm 1.0.78 → 1.0.80
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 +466 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +118 -2
- package/dist/index.d.ts +118 -2
- package/dist/index.js +460 -9
- package/dist/index.js.map +1 -1
- package/package.json +8 -2
- package/src/core/dialect/postgres/index.ts +9 -5
- package/src/index.ts +6 -4
- package/src/openapi/index.ts +3 -0
- package/src/openapi/schema-extractor.ts +418 -0
- package/src/openapi/schema-types.ts +92 -0
- package/src/openapi/type-mappers.ts +207 -0
- package/src/query-builder/select/select-operations.ts +35 -37
- package/src/query-builder/select.ts +156 -142
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { ColumnDef } from '../schema/column-types.js';
|
|
2
|
+
import type { JsonSchemaProperty, JsonSchemaType, JsonSchemaFormat } from './schema-types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Maps SQL column types to OpenAPI JSON Schema types
|
|
6
|
+
*/
|
|
7
|
+
export const mapColumnType = (column: ColumnDef): JsonSchemaProperty => {
|
|
8
|
+
const sqlType = normalizeType(column.type);
|
|
9
|
+
const baseSchema = mapSqlTypeToBaseSchema(sqlType, column);
|
|
10
|
+
|
|
11
|
+
const schema: JsonSchemaProperty = {
|
|
12
|
+
...baseSchema,
|
|
13
|
+
description: column.comment,
|
|
14
|
+
nullable: !column.notNull && !column.primary,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (column.args && sqlType === 'varchar' || sqlType === 'char') {
|
|
18
|
+
schema.maxLength = column.args[0] as number | undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (column.args && sqlType === 'decimal' || sqlType === 'float') {
|
|
22
|
+
if (column.args.length >= 1) {
|
|
23
|
+
schema.minimum = -(10 ** (column.args[0] as number));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (sqlType === 'enum' && column.args && column.args.length > 0) {
|
|
28
|
+
schema.enum = column.args as (string | number | boolean)[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (column.default !== undefined) {
|
|
32
|
+
schema.default = column.default;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return schema;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const normalizeType = (type: string): string => {
|
|
39
|
+
return type.toLowerCase();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const mapSqlTypeToBaseSchema = (
|
|
43
|
+
sqlType: string,
|
|
44
|
+
column: ColumnDef
|
|
45
|
+
): Omit<JsonSchemaProperty, 'nullable' | 'description'> => {
|
|
46
|
+
const type = normalizeType(sqlType);
|
|
47
|
+
|
|
48
|
+
const hasCustomTsType = column.tsType !== undefined;
|
|
49
|
+
|
|
50
|
+
switch (type) {
|
|
51
|
+
case 'int':
|
|
52
|
+
case 'integer':
|
|
53
|
+
case 'bigint':
|
|
54
|
+
return {
|
|
55
|
+
type: hasCustomTsType ? inferTypeFromTsType(column.tsType) : ('integer' as JsonSchemaType),
|
|
56
|
+
format: type === 'bigint' ? 'int64' : 'int32',
|
|
57
|
+
minimum: column.autoIncrement ? 1 : undefined,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
case 'decimal':
|
|
61
|
+
case 'float':
|
|
62
|
+
case 'double':
|
|
63
|
+
return {
|
|
64
|
+
type: hasCustomTsType ? inferTypeFromTsType(column.tsType) : ('number' as JsonSchemaType),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
case 'varchar':
|
|
68
|
+
return {
|
|
69
|
+
type: 'string' as JsonSchemaType,
|
|
70
|
+
minLength: column.notNull ? 1 : undefined,
|
|
71
|
+
maxLength: column.args?.[0] as number | undefined,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
case 'text':
|
|
75
|
+
return {
|
|
76
|
+
type: 'string' as JsonSchemaType,
|
|
77
|
+
minLength: column.notNull ? 1 : undefined,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
case 'char':
|
|
81
|
+
return {
|
|
82
|
+
type: 'string' as JsonSchemaType,
|
|
83
|
+
minLength: column.notNull ? column.args?.[0] as number || 1 : undefined,
|
|
84
|
+
maxLength: column.args?.[0] as number,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
case 'boolean':
|
|
88
|
+
return {
|
|
89
|
+
type: 'boolean' as JsonSchemaType,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
case 'json':
|
|
93
|
+
return {
|
|
94
|
+
anyOf: [
|
|
95
|
+
{ type: 'object' as JsonSchemaType },
|
|
96
|
+
{ type: 'array' as JsonSchemaType },
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
case 'blob':
|
|
101
|
+
case 'binary':
|
|
102
|
+
case 'varbinary':
|
|
103
|
+
return {
|
|
104
|
+
type: 'string' as JsonSchemaType,
|
|
105
|
+
format: 'base64' as JsonSchemaFormat,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
case 'date':
|
|
109
|
+
return {
|
|
110
|
+
type: 'string' as JsonSchemaType,
|
|
111
|
+
format: 'date' as JsonSchemaFormat,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
case 'datetime':
|
|
115
|
+
case 'timestamp':
|
|
116
|
+
return {
|
|
117
|
+
type: 'string' as JsonSchemaType,
|
|
118
|
+
format: 'date-time' as JsonSchemaFormat,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
case 'timestamptz':
|
|
122
|
+
return {
|
|
123
|
+
type: 'string' as JsonSchemaType,
|
|
124
|
+
format: 'date-time' as JsonSchemaFormat,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
case 'uuid':
|
|
128
|
+
return {
|
|
129
|
+
type: 'string' as JsonSchemaType,
|
|
130
|
+
format: 'uuid' as JsonSchemaFormat,
|
|
131
|
+
pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
case 'enum':
|
|
135
|
+
return {
|
|
136
|
+
type: 'string' as JsonSchemaType,
|
|
137
|
+
enum: (column.args as (string | number | boolean)[]) || [],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
default:
|
|
141
|
+
if (column.dialectTypes?.postgres && column.dialectTypes.postgres === 'bytea') {
|
|
142
|
+
return {
|
|
143
|
+
type: 'string' as JsonSchemaType,
|
|
144
|
+
format: 'base64' as JsonSchemaFormat,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
type: 'string' as JsonSchemaType,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const inferTypeFromTsType = (tsType: unknown): JsonSchemaType => {
|
|
155
|
+
if (typeof tsType === 'string') {
|
|
156
|
+
if (tsType === 'number') return 'number' as JsonSchemaType;
|
|
157
|
+
if (tsType === 'string') return 'string' as JsonSchemaType;
|
|
158
|
+
if (tsType === 'boolean') return 'boolean' as JsonSchemaType;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (typeof tsType === 'function') {
|
|
162
|
+
const typeStr = tsType.name?.toLowerCase();
|
|
163
|
+
if (typeStr === 'number') return 'number' as JsonSchemaType;
|
|
164
|
+
if (typeStr === 'string') return 'string' as JsonSchemaType;
|
|
165
|
+
if (typeStr === 'boolean') return 'boolean' as JsonSchemaType;
|
|
166
|
+
if (typeStr === 'array') return 'array' as JsonSchemaType;
|
|
167
|
+
if (typeStr === 'object') return 'object' as JsonSchemaType;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return 'string' as JsonSchemaType;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Maps relation type to array or single object
|
|
175
|
+
*/
|
|
176
|
+
export const mapRelationType = (
|
|
177
|
+
relationType: string
|
|
178
|
+
): { type: 'object' | 'array'; isNullable: boolean } => {
|
|
179
|
+
switch (relationType) {
|
|
180
|
+
case 'HAS_MANY':
|
|
181
|
+
case 'BELONGS_TO_MANY':
|
|
182
|
+
return { type: 'array', isNullable: false };
|
|
183
|
+
case 'HAS_ONE':
|
|
184
|
+
case 'BELONGS_TO':
|
|
185
|
+
return { type: 'object', isNullable: true };
|
|
186
|
+
default:
|
|
187
|
+
return { type: 'object', isNullable: true };
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Gets the OpenAPI format for temporal columns
|
|
193
|
+
*/
|
|
194
|
+
export const getTemporalFormat = (sqlType: string): JsonSchemaFormat | undefined => {
|
|
195
|
+
const type = normalizeType(sqlType);
|
|
196
|
+
|
|
197
|
+
switch (type) {
|
|
198
|
+
case 'date':
|
|
199
|
+
return 'date' as JsonSchemaFormat;
|
|
200
|
+
case 'datetime':
|
|
201
|
+
case 'timestamp':
|
|
202
|
+
case 'timestamptz':
|
|
203
|
+
return 'date-time' as JsonSchemaFormat;
|
|
204
|
+
default:
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
@@ -73,43 +73,41 @@ export async function executeCount(
|
|
|
73
73
|
if (typeof value === 'number') return value;
|
|
74
74
|
if (typeof value === 'bigint') return Number(value);
|
|
75
75
|
if (typeof value === 'string') return Number(value);
|
|
76
|
-
return value === null || value === undefined ? 0 : Number(value);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface PaginatedResult<T> {
|
|
80
|
-
items: T[];
|
|
81
|
-
totalItems: number;
|
|
82
|
-
page: number;
|
|
83
|
-
pageSize: number;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Executes paged queries using the provided builder helpers.
|
|
88
|
-
*/
|
|
89
|
-
export async function executePagedQuery<T, TTable extends TableDef>(
|
|
90
|
-
builder: SelectQueryBuilder<T, TTable>,
|
|
91
|
-
session: OrmSession,
|
|
92
|
-
options: { page: number; pageSize: number },
|
|
93
|
-
countCallback: (session: OrmSession) => Promise<number>
|
|
94
|
-
): Promise<PaginatedResult<T>> {
|
|
95
|
-
const { page, pageSize } = options;
|
|
96
|
-
|
|
97
|
-
if (!Number.isInteger(page) || page < 1) {
|
|
98
|
-
throw new Error('executePaged: page must be an integer >= 1');
|
|
99
|
-
}
|
|
100
|
-
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
101
|
-
throw new Error('executePaged: pageSize must be an integer >= 1');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const offset = (page - 1) * pageSize;
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return { items, totalItems, page, pageSize };
|
|
112
|
-
}
|
|
76
|
+
return value === null || value === undefined ? 0 : Number(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PaginatedResult<T> {
|
|
80
|
+
items: T[];
|
|
81
|
+
totalItems: number;
|
|
82
|
+
page: number;
|
|
83
|
+
pageSize: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Executes paged queries using the provided builder helpers.
|
|
88
|
+
*/
|
|
89
|
+
export async function executePagedQuery<T, TTable extends TableDef>(
|
|
90
|
+
builder: SelectQueryBuilder<T, TTable>,
|
|
91
|
+
session: OrmSession,
|
|
92
|
+
options: { page: number; pageSize: number },
|
|
93
|
+
countCallback: (session: OrmSession) => Promise<number>
|
|
94
|
+
): Promise<PaginatedResult<T>> {
|
|
95
|
+
const { page, pageSize } = options;
|
|
96
|
+
|
|
97
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
98
|
+
throw new Error('executePaged: page must be an integer >= 1');
|
|
99
|
+
}
|
|
100
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
101
|
+
throw new Error('executePaged: pageSize must be an integer >= 1');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const offset = (page - 1) * pageSize;
|
|
105
|
+
|
|
106
|
+
const totalItems = await countCallback(session);
|
|
107
|
+
const items = await builder.limit(pageSize).offset(offset).execute(session);
|
|
108
|
+
|
|
109
|
+
return { items, totalItems, page, pageSize };
|
|
110
|
+
}
|
|
113
111
|
|
|
114
112
|
/**
|
|
115
113
|
* Builds an EXISTS or NOT EXISTS predicate for a related table.
|
|
@@ -29,17 +29,17 @@ import {
|
|
|
29
29
|
SelectQueryBuilderEnvironment
|
|
30
30
|
} from './select-query-builder-deps.js';
|
|
31
31
|
import { ColumnSelector } from './column-selector.js';
|
|
32
|
-
import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
|
|
33
|
-
import { RelationKinds } from '../schema/relation.js';
|
|
34
|
-
import {
|
|
35
|
-
RelationIncludeInput,
|
|
36
|
-
RelationIncludeNodeInput,
|
|
37
|
-
NormalizedRelationIncludeTree,
|
|
38
|
-
cloneRelationIncludeTree,
|
|
39
|
-
mergeRelationIncludeTrees,
|
|
40
|
-
normalizeRelationInclude,
|
|
41
|
-
normalizeRelationIncludeNode
|
|
42
|
-
} from './relation-include-tree.js';
|
|
32
|
+
import { RelationIncludeOptions, RelationTargetColumns, TypedRelationIncludeOptions } from './relation-types.js';
|
|
33
|
+
import { RelationKinds } from '../schema/relation.js';
|
|
34
|
+
import {
|
|
35
|
+
RelationIncludeInput,
|
|
36
|
+
RelationIncludeNodeInput,
|
|
37
|
+
NormalizedRelationIncludeTree,
|
|
38
|
+
cloneRelationIncludeTree,
|
|
39
|
+
mergeRelationIncludeTrees,
|
|
40
|
+
normalizeRelationInclude,
|
|
41
|
+
normalizeRelationIncludeNode
|
|
42
|
+
} from './relation-include-tree.js';
|
|
43
43
|
import { JOIN_KINDS, JoinKind, ORDER_DIRECTIONS, OrderDirection } from '../core/sql/sql.js';
|
|
44
44
|
import { EntityInstance, RelationMap } from '../schema/types.js';
|
|
45
45
|
import type { ColumnToTs, InferRow } from '../schema/types.js';
|
|
@@ -50,23 +50,24 @@ import { executeHydrated, executeHydratedPlain, executeHydratedWithContexts } fr
|
|
|
50
50
|
import { EntityConstructor } from '../orm/entity-metadata.js';
|
|
51
51
|
import { materializeAs } from '../orm/entity-materializer.js';
|
|
52
52
|
import { resolveSelectQuery } from './query-resolution.js';
|
|
53
|
-
import {
|
|
54
|
-
applyOrderBy,
|
|
55
|
-
buildWhereHasPredicate,
|
|
56
|
-
executeCount,
|
|
57
|
-
executePagedQuery,
|
|
58
|
-
PaginatedResult,
|
|
59
|
-
RelationCallback,
|
|
60
|
-
WhereHasOptions
|
|
61
|
-
} from './select/select-operations.js';
|
|
62
|
-
export type { PaginatedResult };
|
|
53
|
+
import {
|
|
54
|
+
applyOrderBy,
|
|
55
|
+
buildWhereHasPredicate,
|
|
56
|
+
executeCount,
|
|
57
|
+
executePagedQuery,
|
|
58
|
+
PaginatedResult,
|
|
59
|
+
RelationCallback,
|
|
60
|
+
WhereHasOptions
|
|
61
|
+
} from './select/select-operations.js';
|
|
62
|
+
export type { PaginatedResult };
|
|
63
63
|
import { SelectFromFacet } from './select/from-facet.js';
|
|
64
64
|
import { SelectJoinFacet } from './select/join-facet.js';
|
|
65
65
|
import { SelectProjectionFacet } from './select/projection-facet.js';
|
|
66
66
|
import { SelectPredicateFacet } from './select/predicate-facet.js';
|
|
67
67
|
import { SelectCTEFacet } from './select/cte-facet.js';
|
|
68
68
|
import { SelectSetOpFacet } from './select/setop-facet.js';
|
|
69
|
-
import { SelectRelationFacet } from './select/relation-facet.js';
|
|
69
|
+
import { SelectRelationFacet } from './select/relation-facet.js';
|
|
70
|
+
import { extractSchema, SchemaOptions, OpenApiSchema } from '../openapi/index.js';
|
|
70
71
|
|
|
71
72
|
type ColumnSelectionValue =
|
|
72
73
|
| ColumnDef
|
|
@@ -115,11 +116,11 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
115
116
|
private readonly predicateFacet: SelectPredicateFacet;
|
|
116
117
|
private readonly cteFacet: SelectCTEFacet;
|
|
117
118
|
private readonly setOpFacet: SelectSetOpFacet;
|
|
118
|
-
private readonly relationFacet: SelectRelationFacet;
|
|
119
|
-
private readonly lazyRelations: Set<string>;
|
|
120
|
-
private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
|
|
121
|
-
private readonly entityConstructor?: EntityConstructor;
|
|
122
|
-
private readonly includeTree: NormalizedRelationIncludeTree;
|
|
119
|
+
private readonly relationFacet: SelectRelationFacet;
|
|
120
|
+
private readonly lazyRelations: Set<string>;
|
|
121
|
+
private readonly lazyRelationOptions: Map<string, RelationIncludeOptions>;
|
|
122
|
+
private readonly entityConstructor?: EntityConstructor;
|
|
123
|
+
private readonly includeTree: NormalizedRelationIncludeTree;
|
|
123
124
|
|
|
124
125
|
/**
|
|
125
126
|
* Creates a new SelectQueryBuilder instance
|
|
@@ -128,16 +129,16 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
128
129
|
* @param hydration - Optional hydration manager
|
|
129
130
|
* @param dependencies - Optional query builder dependencies
|
|
130
131
|
*/
|
|
131
|
-
constructor(
|
|
132
|
-
table: TTable,
|
|
133
|
-
state?: SelectQueryState,
|
|
134
|
-
hydration?: HydrationManager,
|
|
135
|
-
dependencies?: Partial<SelectQueryBuilderDependencies>,
|
|
136
|
-
lazyRelations?: Set<string>,
|
|
137
|
-
lazyRelationOptions?: Map<string, RelationIncludeOptions>,
|
|
138
|
-
entityConstructor?: EntityConstructor,
|
|
139
|
-
includeTree?: NormalizedRelationIncludeTree
|
|
140
|
-
) {
|
|
132
|
+
constructor(
|
|
133
|
+
table: TTable,
|
|
134
|
+
state?: SelectQueryState,
|
|
135
|
+
hydration?: HydrationManager,
|
|
136
|
+
dependencies?: Partial<SelectQueryBuilderDependencies>,
|
|
137
|
+
lazyRelations?: Set<string>,
|
|
138
|
+
lazyRelationOptions?: Map<string, RelationIncludeOptions>,
|
|
139
|
+
entityConstructor?: EntityConstructor,
|
|
140
|
+
includeTree?: NormalizedRelationIncludeTree
|
|
141
|
+
) {
|
|
141
142
|
const deps = resolveSelectQueryBuilderDependencies(dependencies);
|
|
142
143
|
this.env = { table, deps };
|
|
143
144
|
const createAstService = (nextState: SelectQueryState) => deps.createQueryAstService(table, nextState);
|
|
@@ -147,11 +148,11 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
147
148
|
state: initialState,
|
|
148
149
|
hydration: initialHydration
|
|
149
150
|
};
|
|
150
|
-
this.lazyRelations = new Set(lazyRelations ?? []);
|
|
151
|
-
this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
|
|
152
|
-
this.entityConstructor = entityConstructor;
|
|
153
|
-
this.includeTree = includeTree ?? {};
|
|
154
|
-
this.columnSelector = deps.createColumnSelector(this.env);
|
|
151
|
+
this.lazyRelations = new Set(lazyRelations ?? []);
|
|
152
|
+
this.lazyRelationOptions = new Map(lazyRelationOptions ?? []);
|
|
153
|
+
this.entityConstructor = entityConstructor;
|
|
154
|
+
this.includeTree = includeTree ?? {};
|
|
155
|
+
this.columnSelector = deps.createColumnSelector(this.env);
|
|
155
156
|
const relationManager = deps.createRelationManager(this.env);
|
|
156
157
|
this.fromFacet = new SelectFromFacet(this.env, createAstService);
|
|
157
158
|
this.joinFacet = new SelectJoinFacet(this.env, createAstService);
|
|
@@ -168,23 +169,23 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
168
169
|
* @param lazyRelations - Updated lazy relations set
|
|
169
170
|
* @returns New SelectQueryBuilder instance
|
|
170
171
|
*/
|
|
171
|
-
private clone<TNext = T>(
|
|
172
|
-
context: SelectQueryBuilderContext = this.context,
|
|
173
|
-
lazyRelations = new Set(this.lazyRelations),
|
|
174
|
-
lazyRelationOptions = new Map(this.lazyRelationOptions),
|
|
175
|
-
includeTree = this.includeTree
|
|
176
|
-
): SelectQueryBuilder<TNext, TTable> {
|
|
177
|
-
return new SelectQueryBuilder(
|
|
178
|
-
this.env.table as TTable,
|
|
179
|
-
context.state,
|
|
180
|
-
context.hydration,
|
|
181
|
-
this.env.deps,
|
|
182
|
-
lazyRelations,
|
|
183
|
-
lazyRelationOptions,
|
|
184
|
-
this.entityConstructor,
|
|
185
|
-
includeTree
|
|
186
|
-
) as SelectQueryBuilder<TNext, TTable>;
|
|
187
|
-
}
|
|
172
|
+
private clone<TNext = T>(
|
|
173
|
+
context: SelectQueryBuilderContext = this.context,
|
|
174
|
+
lazyRelations = new Set(this.lazyRelations),
|
|
175
|
+
lazyRelationOptions = new Map(this.lazyRelationOptions),
|
|
176
|
+
includeTree = this.includeTree
|
|
177
|
+
): SelectQueryBuilder<TNext, TTable> {
|
|
178
|
+
return new SelectQueryBuilder(
|
|
179
|
+
this.env.table as TTable,
|
|
180
|
+
context.state,
|
|
181
|
+
context.hydration,
|
|
182
|
+
this.env.deps,
|
|
183
|
+
lazyRelations,
|
|
184
|
+
lazyRelationOptions,
|
|
185
|
+
this.entityConstructor,
|
|
186
|
+
includeTree
|
|
187
|
+
) as SelectQueryBuilder<TNext, TTable>;
|
|
188
|
+
}
|
|
188
189
|
|
|
189
190
|
/**
|
|
190
191
|
* Applies an alias to the root FROM table.
|
|
@@ -564,42 +565,42 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
564
565
|
* qb.include('posts');
|
|
565
566
|
* @example
|
|
566
567
|
* qb.include('posts', { columns: ['id', 'title', 'published'] });
|
|
567
|
-
* @example
|
|
568
|
-
* qb.include('posts', {
|
|
569
|
-
* columns: ['id', 'title'],
|
|
570
|
-
* where: eq(postTable.columns.published, true)
|
|
571
|
-
* });
|
|
572
|
-
* @example
|
|
573
|
-
* qb.include({ posts: { include: { author: true } } });
|
|
574
|
-
*/
|
|
575
|
-
include<K extends keyof TTable['relations'] & string>(
|
|
576
|
-
relationName: K,
|
|
577
|
-
options?: RelationIncludeNodeInput<TTable['relations'][K]>
|
|
578
|
-
): SelectQueryBuilder<T, TTable>;
|
|
579
|
-
include(relations: RelationIncludeInput<TTable>): SelectQueryBuilder<T, TTable>;
|
|
580
|
-
include<K extends keyof TTable['relations'] & string>(
|
|
581
|
-
relationNameOrRelations: K | RelationIncludeInput<TTable>,
|
|
582
|
-
options?: RelationIncludeNodeInput<TTable['relations'][K]>
|
|
583
|
-
): SelectQueryBuilder<T, TTable> {
|
|
584
|
-
if (typeof relationNameOrRelations === 'object' && relationNameOrRelations !== null) {
|
|
585
|
-
const normalized = normalizeRelationInclude(relationNameOrRelations as RelationIncludeInput<TableDef>);
|
|
586
|
-
let nextContext = this.context;
|
|
587
|
-
for (const [relationName, node] of Object.entries(normalized)) {
|
|
588
|
-
nextContext = this.relationFacet.include(nextContext, relationName, node.options);
|
|
589
|
-
}
|
|
590
|
-
const nextTree = mergeRelationIncludeTrees(this.includeTree, normalized);
|
|
591
|
-
return this.clone(nextContext, undefined, undefined, nextTree);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const relationName = relationNameOrRelations as string;
|
|
595
|
-
const normalizedNode = normalizeRelationIncludeNode(options);
|
|
596
|
-
const nextContext = this.relationFacet.include(this.context, relationName, normalizedNode.options);
|
|
597
|
-
const shouldStore = Boolean(normalizedNode.include || normalizedNode.options);
|
|
598
|
-
const nextTree = shouldStore
|
|
599
|
-
? mergeRelationIncludeTrees(this.includeTree, { [relationName]: normalizedNode })
|
|
600
|
-
: this.includeTree;
|
|
601
|
-
return this.clone(nextContext, undefined, undefined, nextTree);
|
|
602
|
-
}
|
|
568
|
+
* @example
|
|
569
|
+
* qb.include('posts', {
|
|
570
|
+
* columns: ['id', 'title'],
|
|
571
|
+
* where: eq(postTable.columns.published, true)
|
|
572
|
+
* });
|
|
573
|
+
* @example
|
|
574
|
+
* qb.include({ posts: { include: { author: true } } });
|
|
575
|
+
*/
|
|
576
|
+
include<K extends keyof TTable['relations'] & string>(
|
|
577
|
+
relationName: K,
|
|
578
|
+
options?: RelationIncludeNodeInput<TTable['relations'][K]>
|
|
579
|
+
): SelectQueryBuilder<T, TTable>;
|
|
580
|
+
include(relations: RelationIncludeInput<TTable>): SelectQueryBuilder<T, TTable>;
|
|
581
|
+
include<K extends keyof TTable['relations'] & string>(
|
|
582
|
+
relationNameOrRelations: K | RelationIncludeInput<TTable>,
|
|
583
|
+
options?: RelationIncludeNodeInput<TTable['relations'][K]>
|
|
584
|
+
): SelectQueryBuilder<T, TTable> {
|
|
585
|
+
if (typeof relationNameOrRelations === 'object' && relationNameOrRelations !== null) {
|
|
586
|
+
const normalized = normalizeRelationInclude(relationNameOrRelations as RelationIncludeInput<TableDef>);
|
|
587
|
+
let nextContext = this.context;
|
|
588
|
+
for (const [relationName, node] of Object.entries(normalized)) {
|
|
589
|
+
nextContext = this.relationFacet.include(nextContext, relationName, node.options);
|
|
590
|
+
}
|
|
591
|
+
const nextTree = mergeRelationIncludeTrees(this.includeTree, normalized);
|
|
592
|
+
return this.clone(nextContext, undefined, undefined, nextTree);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const relationName = relationNameOrRelations as string;
|
|
596
|
+
const normalizedNode = normalizeRelationIncludeNode(options);
|
|
597
|
+
const nextContext = this.relationFacet.include(this.context, relationName, normalizedNode.options);
|
|
598
|
+
const shouldStore = Boolean(normalizedNode.include || normalizedNode.options);
|
|
599
|
+
const nextTree = shouldStore
|
|
600
|
+
? mergeRelationIncludeTrees(this.includeTree, { [relationName]: normalizedNode })
|
|
601
|
+
: this.includeTree;
|
|
602
|
+
return this.clone(nextContext, undefined, undefined, nextTree);
|
|
603
|
+
}
|
|
603
604
|
|
|
604
605
|
/**
|
|
605
606
|
* Includes a relation lazily in the query results
|
|
@@ -647,13 +648,13 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
647
648
|
* @example
|
|
648
649
|
* qb.includePick('posts', ['id', 'title', 'createdAt']);
|
|
649
650
|
*/
|
|
650
|
-
includePick<
|
|
651
|
-
K extends keyof TTable['relations'] & string,
|
|
652
|
-
C extends RelationTargetColumns<TTable['relations'][K]>
|
|
653
|
-
>(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
|
|
654
|
-
const options = { columns: cols as readonly C[] } as unknown as RelationIncludeNodeInput<TTable['relations'][K]>;
|
|
655
|
-
return this.include(relationName, options);
|
|
656
|
-
}
|
|
651
|
+
includePick<
|
|
652
|
+
K extends keyof TTable['relations'] & string,
|
|
653
|
+
C extends RelationTargetColumns<TTable['relations'][K]>
|
|
654
|
+
>(relationName: K, cols: C[]): SelectQueryBuilder<T, TTable> {
|
|
655
|
+
const options = { columns: cols as readonly C[] } as unknown as RelationIncludeNodeInput<TTable['relations'][K]>;
|
|
656
|
+
return this.include(relationName, options);
|
|
657
|
+
}
|
|
657
658
|
|
|
658
659
|
/**
|
|
659
660
|
* Selects columns for the root table and relations from an array of entries
|
|
@@ -670,13 +671,13 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
670
671
|
let currBuilder: SelectQueryBuilder<T, TTable> = this;
|
|
671
672
|
|
|
672
673
|
for (const entry of config) {
|
|
673
|
-
if (entry.type === 'root') {
|
|
674
|
-
currBuilder = currBuilder.select(...entry.columns);
|
|
675
|
-
} else {
|
|
676
|
-
const options = { columns: entry.columns } as unknown as RelationIncludeNodeInput<TTable['relations'][typeof entry.relationName]>;
|
|
677
|
-
currBuilder = currBuilder.include(entry.relationName, options);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
674
|
+
if (entry.type === 'root') {
|
|
675
|
+
currBuilder = currBuilder.select(...entry.columns);
|
|
676
|
+
} else {
|
|
677
|
+
const options = { columns: entry.columns } as unknown as RelationIncludeNodeInput<TTable['relations'][typeof entry.relationName]>;
|
|
678
|
+
currBuilder = currBuilder.include(entry.relationName, options);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
680
681
|
|
|
681
682
|
return currBuilder;
|
|
682
683
|
}
|
|
@@ -693,16 +694,16 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
693
694
|
* Gets lazy relation include options
|
|
694
695
|
* @returns Map of relation names to include options
|
|
695
696
|
*/
|
|
696
|
-
getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
|
|
697
|
-
return new Map(this.lazyRelationOptions);
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Gets normalized nested include information for runtime preloading.
|
|
702
|
-
*/
|
|
703
|
-
getIncludeTree(): NormalizedRelationIncludeTree {
|
|
704
|
-
return cloneRelationIncludeTree(this.includeTree);
|
|
705
|
-
}
|
|
697
|
+
getLazyRelationOptions(): Map<string, RelationIncludeOptions> {
|
|
698
|
+
return new Map(this.lazyRelationOptions);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Gets normalized nested include information for runtime preloading.
|
|
703
|
+
*/
|
|
704
|
+
getIncludeTree(): NormalizedRelationIncludeTree {
|
|
705
|
+
return cloneRelationIncludeTree(this.includeTree);
|
|
706
|
+
}
|
|
706
707
|
|
|
707
708
|
/**
|
|
708
709
|
* Gets the table definition for this query builder
|
|
@@ -794,19 +795,19 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
794
795
|
return executeCount(this.context, this.env, session);
|
|
795
796
|
}
|
|
796
797
|
|
|
797
|
-
/**
|
|
798
|
-
* Executes the query and returns both the paged items and the total.
|
|
799
|
-
*
|
|
800
|
-
* @example
|
|
801
|
-
* const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
|
|
802
|
-
*/
|
|
803
|
-
async executePaged(
|
|
804
|
-
session: OrmSession,
|
|
805
|
-
options: { page: number; pageSize: number }
|
|
806
|
-
): Promise<PaginatedResult<T>> {
|
|
807
|
-
const builder = this.ensureDefaultSelection();
|
|
808
|
-
return executePagedQuery(builder, session, options, sess =>
|
|
809
|
-
}
|
|
798
|
+
/**
|
|
799
|
+
* Executes the query and returns both the paged items and the total.
|
|
800
|
+
*
|
|
801
|
+
* @example
|
|
802
|
+
* const { items, totalItems, page, pageSize } = await qb.executePaged(session, { page: 1, pageSize: 20 });
|
|
803
|
+
*/
|
|
804
|
+
async executePaged(
|
|
805
|
+
session: OrmSession,
|
|
806
|
+
options: { page: number; pageSize: number }
|
|
807
|
+
): Promise<PaginatedResult<T>> {
|
|
808
|
+
const builder = this.ensureDefaultSelection();
|
|
809
|
+
return executePagedQuery(builder, session, options, sess => builder.count(sess));
|
|
810
|
+
}
|
|
810
811
|
|
|
811
812
|
/**
|
|
812
813
|
* Executes the query with provided execution and hydration contexts
|
|
@@ -1108,16 +1109,29 @@ export class SelectQueryBuilder<T = EntityInstance<TableDef>, TTable extends Tab
|
|
|
1108
1109
|
return this.compile(dialect).sql;
|
|
1109
1110
|
}
|
|
1110
1111
|
|
|
1111
|
-
/**
|
|
1112
|
-
* Gets
|
|
1113
|
-
* @returns Hydration plan or undefined if none exists
|
|
1114
|
-
* @example
|
|
1115
|
-
* const plan = qb.include('posts').getHydrationPlan();
|
|
1116
|
-
* console.log(plan?.relations); // Information about included relations
|
|
1117
|
-
*/
|
|
1118
|
-
getHydrationPlan(): HydrationPlan | undefined {
|
|
1119
|
-
return this.context.hydration.getPlan();
|
|
1120
|
-
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Gets hydration plan for query
|
|
1114
|
+
* @returns Hydration plan or undefined if none exists
|
|
1115
|
+
* @example
|
|
1116
|
+
* const plan = qb.include('posts').getHydrationPlan();
|
|
1117
|
+
* console.log(plan?.relations); // Information about included relations
|
|
1118
|
+
*/
|
|
1119
|
+
getHydrationPlan(): HydrationPlan | undefined {
|
|
1120
|
+
return this.context.hydration.getPlan();
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Gets OpenAPI 3.1 JSON Schema for query result
|
|
1125
|
+
* @param options - Schema generation options
|
|
1126
|
+
* @returns OpenAPI 3.1 JSON Schema for query result
|
|
1127
|
+
* @example
|
|
1128
|
+
* const schema = qb.select('id', 'title', 'author').getSchema();
|
|
1129
|
+
* console.log(JSON.stringify(schema, null, 2));
|
|
1130
|
+
*/
|
|
1131
|
+
getSchema(options?: SchemaOptions): OpenApiSchema {
|
|
1132
|
+
const plan = this.context.hydration.getPlan();
|
|
1133
|
+
return extractSchema(this.env.table, plan, this.context.state.ast.columns, options);
|
|
1134
|
+
}
|
|
1121
1135
|
|
|
1122
1136
|
/**
|
|
1123
1137
|
* Gets the Abstract Syntax Tree (AST) representation of the query
|